@stonecrop/atable 0.14.0 → 0.16.0

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.
@@ -1,795 +0,0 @@
1
- import { defineStore } from 'pinia';
2
- import { computed, ref } from 'vue';
3
- import { generateHash } from '../utils';
4
- /**
5
- * Create a table store
6
- * @param initData - Initial data for the table store
7
- * @returns table store instance
8
- * @public
9
- */
10
- export const createTableStore = (initData) => {
11
- const id = initData.id || generateHash();
12
- const createStore = defineStore(`table-${id}`, () => {
13
- const createDisplayObject = () => {
14
- const defaultDisplay = [Object.assign({}, { rowModified: false })];
15
- // TODO: (typing) is this type correct for the parent set?
16
- const parents = new Set();
17
- for (let rowIndex = 0; rowIndex < rows.value.length; rowIndex++) {
18
- const row = rows.value[rowIndex];
19
- if (row.parent !== null && row.parent !== undefined) {
20
- parents.add(row.parent);
21
- }
22
- }
23
- // Helper function to check if a row has gantt data
24
- const hasGanttData = (rowIndex) => {
25
- return rows.value[rowIndex]?.gantt !== undefined;
26
- };
27
- // Helper function to check if any descendant has gantt data
28
- const hasGanttDescendant = (rowIndex) => {
29
- for (let i = 0; i < rows.value.length; i++) {
30
- if (rows.value[i].parent === rowIndex) {
31
- if (hasGanttData(i) || hasGanttDescendant(i)) {
32
- return true;
33
- }
34
- }
35
- }
36
- return false;
37
- };
38
- // Helper function to determine if children should be open based on expansion mode
39
- const shouldChildrenBeOpen = (rowIndex) => {
40
- const currentConfig = config.value;
41
- const expansionMode = currentConfig.view === 'tree' || currentConfig.view === 'tree-gantt'
42
- ? currentConfig.defaultTreeExpansion
43
- : undefined;
44
- if (!expansionMode)
45
- return true; // Default behavior - start expanded (leaf mode)
46
- switch (expansionMode) {
47
- case 'root':
48
- return false; // Only root nodes are visible, all children start collapsed
49
- case 'branch':
50
- // Only expand if this node leads to gantt nodes OR if this node itself has gantt data AND has gantt children
51
- return hasGanttDescendant(rowIndex);
52
- case 'leaf':
53
- return true; // All nodes should be expanded
54
- default:
55
- return true; // Default to expanded if unknown mode
56
- }
57
- };
58
- for (let rowIndex = 0; rowIndex < rows.value.length; rowIndex++) {
59
- const row = rows.value[rowIndex];
60
- const isRootNode = row.parent === null || row.parent === undefined;
61
- const isParentNode = parents.has(rowIndex);
62
- defaultDisplay[rowIndex] = {
63
- childrenOpen: shouldChildrenBeOpen(rowIndex),
64
- expanded: false,
65
- indent: row.indent || 0,
66
- isParent: isParentNode,
67
- isRoot: isRootNode,
68
- rowModified: false,
69
- open: isRootNode, // This will be recalculated later for non-root nodes
70
- parent: row.parent,
71
- };
72
- }
73
- return defaultDisplay;
74
- };
75
- // state
76
- const columns = ref(initData.columns);
77
- const rows = ref(initData.rows);
78
- const config = ref(initData.config || {});
79
- // Track row modifications and expand states separately from the computed display
80
- const rowModifications = ref({});
81
- const rowExpandStates = ref({});
82
- const table = computed(() => {
83
- const table = {};
84
- for (const [colIndex, column] of columns.value.entries()) {
85
- for (const [rowIndex, row] of rows.value.entries()) {
86
- table[`${colIndex}:${rowIndex}`] = row[column.name];
87
- }
88
- }
89
- return table;
90
- });
91
- const display = computed({
92
- get: () => {
93
- const baseDisplay = createDisplayObject();
94
- // Apply persistent modifications and expand states
95
- for (let i = 0; i < baseDisplay.length; i++) {
96
- if (rowModifications.value[i]) {
97
- baseDisplay[i].rowModified = rowModifications.value[i];
98
- }
99
- if (rowExpandStates.value[i]) {
100
- if (rowExpandStates.value[i].childrenOpen !== undefined) {
101
- baseDisplay[i].childrenOpen = rowExpandStates.value[i].childrenOpen;
102
- }
103
- if (rowExpandStates.value[i].expanded !== undefined) {
104
- baseDisplay[i].expanded = rowExpandStates.value[i].expanded;
105
- }
106
- }
107
- }
108
- // Calculate 'open' property for tree view based on parent's childrenOpen state
109
- if (isTreeView.value) {
110
- // Helper function to check if all ancestors are open
111
- const isNodeOpen = (rowIndex, display) => {
112
- const row = display[rowIndex];
113
- if (row.isRoot) {
114
- return true; // Root nodes are always open
115
- }
116
- if (row.parent === null || row.parent === undefined) {
117
- return true;
118
- }
119
- const parentIndex = row.parent;
120
- if (parentIndex < 0 || parentIndex >= display.length) {
121
- return false;
122
- }
123
- const parent = display[parentIndex];
124
- // Node is open if parent's children are open AND parent itself is open
125
- return (parent.childrenOpen || false) && isNodeOpen(parentIndex, display);
126
- };
127
- for (let i = 0; i < baseDisplay.length; i++) {
128
- const row = baseDisplay[i];
129
- if (!row.isRoot) {
130
- baseDisplay[i].open = isNodeOpen(i, baseDisplay);
131
- }
132
- }
133
- }
134
- return baseDisplay;
135
- },
136
- set: (newDisplay) => {
137
- // Only update if the new display is different from the current one; also avoids recursive updates
138
- if (JSON.stringify(newDisplay) !== JSON.stringify(display.value)) {
139
- display.value = newDisplay;
140
- }
141
- },
142
- });
143
- const modal = ref(initData.modal || { visible: false });
144
- const updates = ref({});
145
- const ganttBars = ref([]);
146
- const connectionHandles = ref([]);
147
- const connectionPaths = ref([]);
148
- const sortState = ref({
149
- column: null,
150
- direction: null,
151
- });
152
- const filterState = ref({});
153
- // getters
154
- const hasPinnedColumns = computed(() => columns.value.some(col => col.pinned));
155
- const isGanttView = computed(() => config.value.view === 'gantt' || config.value.view === 'tree-gantt');
156
- const isTreeView = computed(() => config.value.view === 'tree' || config.value.view === 'tree-gantt');
157
- const isDependencyGraphEnabled = computed(() => {
158
- const currentConfig = config.value;
159
- return currentConfig.view === 'gantt' || currentConfig.view === 'tree-gantt'
160
- ? currentConfig.dependencyGraph !== false
161
- : true;
162
- });
163
- const numberedRowWidth = computed(() => {
164
- const indent = Math.ceil(rows.value.length / 100 + 1);
165
- return `${indent}ch`;
166
- });
167
- const zeroColumn = computed(() => config.value.view ? ['list', 'tree', 'tree-gantt', 'list-expansion'].includes(config.value.view) : false);
168
- const filteredRows = computed(() => {
169
- let filtered = rows.value.map((row, originalIndex) => ({
170
- ...row,
171
- originalIndex,
172
- }));
173
- // Apply filters
174
- Object.entries(filterState.value).forEach(([colIndexStr, filter]) => {
175
- const colIndex = parseInt(colIndexStr);
176
- const column = columns.value[colIndex];
177
- if (!column)
178
- return;
179
- // Skip if filter has no value (except for dateRange and checkbox which can have different value structures)
180
- const hasFilterValue = filter.value ||
181
- filter.startValue ||
182
- filter.endValue ||
183
- (column.filterType === 'checkbox' && filter.value !== undefined);
184
- if (!hasFilterValue)
185
- return;
186
- filtered = filtered.filter(row => {
187
- const cellValue = row[column.name];
188
- return applyFilter(cellValue, filter, column);
189
- });
190
- });
191
- // Apply sorting if active
192
- if (sortState.value.column !== null && sortState.value.direction) {
193
- const column = columns.value[sortState.value.column];
194
- const direction = sortState.value.direction;
195
- filtered.sort((a, b) => {
196
- let aVal = a[column.name];
197
- let bVal = b[column.name];
198
- if (aVal === null || aVal === undefined)
199
- aVal = '';
200
- if (bVal === null || bVal === undefined)
201
- bVal = '';
202
- const aNum = Number(aVal);
203
- const bNum = Number(bVal);
204
- const isNumeric = !isNaN(aNum) && !isNaN(bNum) && aVal !== '' && bVal !== '';
205
- if (isNumeric) {
206
- return direction === 'asc' ? aNum - bNum : bNum - aNum;
207
- }
208
- else {
209
- const aStr = String(aVal).toLowerCase();
210
- const bStr = String(bVal).toLowerCase();
211
- return direction === 'asc' ? aStr.localeCompare(bStr) : bStr.localeCompare(aStr);
212
- }
213
- });
214
- }
215
- return filtered;
216
- });
217
- // actions
218
- const getCellData = (colIndex, rowIndex) => table.value[`${colIndex}:${rowIndex}`];
219
- const setCellData = (colIndex, rowIndex, value) => {
220
- const index = `${colIndex}:${rowIndex}`;
221
- const col = columns.value[colIndex];
222
- if (table.value[index] !== value) {
223
- rowModifications.value[rowIndex] = true;
224
- }
225
- table.value[index] = value;
226
- // Create a new row object to ensure reactivity
227
- rows.value[rowIndex] = {
228
- ...rows.value[rowIndex],
229
- [col.name]: value,
230
- };
231
- };
232
- const updateRows = (newRows) => {
233
- rows.value = newRows;
234
- };
235
- const setCellText = (colIndex, rowIndex, value) => {
236
- const index = `${colIndex}:${rowIndex}`;
237
- if (table.value[index] !== value) {
238
- rowModifications.value[rowIndex] = true;
239
- updates.value[index] = value;
240
- }
241
- };
242
- const getHeaderCellStyle = (column) => {
243
- const isLastCol = columns.value.indexOf(column) === columns.value.length - 1;
244
- // if the table is full width, the last column should not be resizable;
245
- // ref: https://github.com/agritheory/stonecrop/pull/196#issuecomment-2503762641
246
- const isResizable = config.value.fullWidth ? column.resizable && !isLastCol : column.resizable;
247
- return {
248
- width: column.width || '40ch',
249
- textAlign: column.align || 'center',
250
- ...(isResizable && {
251
- resize: 'horizontal',
252
- overflow: 'hidden',
253
- whiteSpace: 'nowrap',
254
- }),
255
- };
256
- };
257
- const resizeColumn = (colIndex, newWidth) => {
258
- if (colIndex < 0 || colIndex >= columns.value.length)
259
- return;
260
- const minWidth = 40;
261
- const finalWidth = Math.max(newWidth, minWidth);
262
- columns.value[colIndex] = {
263
- ...columns.value[colIndex],
264
- width: `${finalWidth}px`,
265
- };
266
- };
267
- const isRowGantt = (rowIndex) => {
268
- const row = rows.value[rowIndex];
269
- return isGanttView.value && row.gantt !== undefined;
270
- };
271
- const isRowVisible = (rowIndex) => {
272
- return !isTreeView.value || display.value[rowIndex].isRoot || display.value[rowIndex].open;
273
- };
274
- const getRowExpandSymbol = (rowIndex) => {
275
- if (!isTreeView.value && config.value.view !== 'list-expansion') {
276
- return '';
277
- }
278
- if (isTreeView.value && (display.value[rowIndex].isRoot || display.value[rowIndex].isParent)) {
279
- return display.value[rowIndex].childrenOpen ? '▼' : '►';
280
- }
281
- if (config.value.view === 'list-expansion') {
282
- return display.value[rowIndex].expanded ? '▼' : '►';
283
- }
284
- return '';
285
- };
286
- const toggleRowExpand = (rowIndex) => {
287
- if (isTreeView.value) {
288
- const currentState = rowExpandStates.value[rowIndex] || {};
289
- const currentChildrenOpen = currentState.childrenOpen ?? display.value[rowIndex].childrenOpen;
290
- const newChildrenOpen = !currentChildrenOpen;
291
- rowExpandStates.value[rowIndex] = {
292
- ...currentState,
293
- childrenOpen: newChildrenOpen,
294
- };
295
- // If we're closing, recursively close all descendant nodes
296
- if (!newChildrenOpen) {
297
- closeDescendants(rowIndex);
298
- }
299
- }
300
- else if (config.value.view === 'list-expansion') {
301
- const currentState = rowExpandStates.value[rowIndex] || {};
302
- const currentExpanded = currentState.expanded ?? display.value[rowIndex].expanded;
303
- rowExpandStates.value[rowIndex] = {
304
- ...currentState,
305
- expanded: !currentExpanded,
306
- };
307
- }
308
- };
309
- const closeDescendants = (parentRowIndex) => {
310
- for (let index = 0; index < rows.value.length; index++) {
311
- if (display.value[index].parent === parentRowIndex) {
312
- const childState = rowExpandStates.value[index] || {};
313
- rowExpandStates.value[index] = {
314
- ...childState,
315
- childrenOpen: false,
316
- };
317
- // Recursively close this child's descendants
318
- closeDescendants(index);
319
- }
320
- }
321
- };
322
- const getCellDisplayValue = (colIndex, rowIndex) => {
323
- const cellData = getCellData(colIndex, rowIndex);
324
- return getFormattedValue(colIndex, rowIndex, cellData);
325
- };
326
- const getFormattedValue = (colIndex, rowIndex, value) => {
327
- const column = columns.value[colIndex];
328
- const row = rows.value[rowIndex];
329
- const format = column.format;
330
- if (!format) {
331
- return value;
332
- }
333
- if (typeof format === 'function') {
334
- return format(value, { table: table.value, row, column });
335
- }
336
- else if (typeof format === 'string') {
337
- // parse format function from string
338
- // eslint-disable-next-line @typescript-eslint/no-implied-eval, @typescript-eslint/no-unsafe-call
339
- const formatFn = Function(`"use strict";return (${format})`)();
340
- return formatFn(value, { table: table.value, row, column });
341
- }
342
- return value;
343
- };
344
- const closeModal = (event) => {
345
- if (!(event.target instanceof Node)) {
346
- // if the target is not a node, it's probably a custom click event to Document or Window
347
- // err on the side of closing the modal in that case
348
- if (modal.value.visible)
349
- modal.value.visible = false;
350
- }
351
- else if (!modal.value.parent?.contains(event.target)) {
352
- if (modal.value.visible)
353
- modal.value.visible = false;
354
- }
355
- };
356
- const getIndent = (colIndex, indentLevel) => {
357
- if (indentLevel && colIndex === 0 && indentLevel > 0) {
358
- return `${indentLevel}ch`;
359
- }
360
- else {
361
- return 'inherit';
362
- }
363
- };
364
- const updateGanttBar = (event) => {
365
- // update the local gantt bar cache
366
- const ganttBar = rows.value[event.rowIndex]?.gantt;
367
- if (ganttBar) {
368
- if (event.type === 'resize') {
369
- if (event.edge === 'start') {
370
- ganttBar.startIndex = event.newStart;
371
- ganttBar.endIndex = event.end;
372
- ganttBar.colspan = ganttBar.endIndex - ganttBar.startIndex;
373
- }
374
- else if (event.edge === 'end') {
375
- ganttBar.startIndex = event.start;
376
- ganttBar.endIndex = event.newEnd;
377
- ganttBar.colspan = ganttBar.endIndex - ganttBar.startIndex;
378
- }
379
- }
380
- else if (event.type === 'bar') {
381
- ganttBar.startIndex = event.newStart;
382
- ganttBar.endIndex = event.newEnd;
383
- ganttBar.colspan = ganttBar.endIndex - ganttBar.startIndex;
384
- }
385
- }
386
- };
387
- const registerGanttBar = (barInfo) => {
388
- const existingIndex = ganttBars.value.findIndex(bar => bar.id === barInfo.id);
389
- if (existingIndex >= 0) {
390
- // @ts-expect-error TODO: for some reason, the IDE is expecting an unref'd value
391
- ganttBars.value[existingIndex] = barInfo;
392
- }
393
- else {
394
- // @ts-expect-error TODO: for some reason, the IDE is expecting an unref'd value
395
- ganttBars.value.push(barInfo);
396
- }
397
- };
398
- const unregisterGanttBar = (barId) => {
399
- const index = ganttBars.value.findIndex(bar => bar.id === barId);
400
- if (index >= 0) {
401
- ganttBars.value.splice(index, 1);
402
- }
403
- };
404
- const registerConnectionHandle = (handleInfo) => {
405
- const existingIndex = connectionHandles.value.findIndex(handle => handle.id === handleInfo.id);
406
- if (existingIndex >= 0) {
407
- // @ts-expect-error TODO: for some reason, the IDE is expecting an unref'd value
408
- connectionHandles.value[existingIndex] = handleInfo;
409
- }
410
- else {
411
- // @ts-expect-error TODO: for some reason, the IDE is expecting an unref'd value
412
- connectionHandles.value.push(handleInfo);
413
- }
414
- };
415
- const unregisterConnectionHandle = (handleId) => {
416
- const index = connectionHandles.value.findIndex(handle => handle.id === handleId);
417
- if (index >= 0) {
418
- connectionHandles.value.splice(index, 1);
419
- }
420
- };
421
- const createConnection = (fromHandleId, toHandleId, options) => {
422
- const fromHandle = connectionHandles.value.find(h => h.id === fromHandleId);
423
- const toHandle = connectionHandles.value.find(h => h.id === toHandleId);
424
- if (!fromHandle || !toHandle) {
425
- // eslint-disable-next-line no-console
426
- console.warn('Cannot create connection: handle not found');
427
- return null;
428
- }
429
- const connection = {
430
- id: `connection-${fromHandleId}-${toHandleId}`,
431
- from: {
432
- barId: fromHandle.barId,
433
- side: fromHandle.side,
434
- },
435
- to: {
436
- barId: toHandle.barId,
437
- side: toHandle.side,
438
- },
439
- style: options?.style,
440
- label: options?.label,
441
- };
442
- connectionPaths.value.push(connection);
443
- return connection;
444
- };
445
- const deleteConnection = (connectionId) => {
446
- const index = connectionPaths.value.findIndex(conn => conn.id === connectionId);
447
- if (index >= 0) {
448
- connectionPaths.value.splice(index, 1);
449
- return true;
450
- }
451
- return false;
452
- };
453
- const getConnectionsForBar = (barId) => {
454
- return connectionPaths.value.filter(conn => conn.from.barId === barId || conn.to.barId === barId);
455
- };
456
- const getHandlesForBar = (barId) => {
457
- return connectionHandles.value.filter(handle => handle.barId === barId);
458
- };
459
- const sortByColumn = (colIndex) => {
460
- const column = columns.value[colIndex];
461
- if (column.sortable === false)
462
- return;
463
- let newDirection;
464
- if (sortState.value.column === colIndex) {
465
- if (sortState.value.direction === 'asc') {
466
- newDirection = 'desc';
467
- }
468
- else {
469
- newDirection = 'asc';
470
- }
471
- }
472
- else {
473
- newDirection = 'asc';
474
- }
475
- sortState.value.column = colIndex;
476
- sortState.value.direction = newDirection;
477
- // Note: The actual sorting is now handled in the filteredRows computed property
478
- // This ensures that sorting works on filtered data without modifying the original rows
479
- };
480
- const applyFilter = (cellValue, filter, column) => {
481
- const filterType = column.filterType || 'text';
482
- const value = filter.value;
483
- if (!value && filterType !== 'dateRange' && filterType !== 'checkbox')
484
- return true;
485
- switch (filterType) {
486
- case 'text': {
487
- // Handle objects with nested properties
488
- let searchableText = '';
489
- if (typeof cellValue === 'object' && cellValue !== null) {
490
- // If it's an object, search in all string values
491
- searchableText = Object.values(cellValue).join(' ');
492
- }
493
- else {
494
- searchableText = String(cellValue || '');
495
- }
496
- return searchableText.toLowerCase().includes(String(value).toLowerCase());
497
- }
498
- case 'number': {
499
- const numValue = Number(cellValue);
500
- const filterNum = Number(value);
501
- return !isNaN(numValue) && !isNaN(filterNum) && numValue === filterNum;
502
- }
503
- case 'select':
504
- return cellValue === value;
505
- case 'checkbox':
506
- // For checkbox filter, if checked (true), show only truthy values
507
- // If unchecked (false/undefined), show all values
508
- if (value === true) {
509
- return !!cellValue;
510
- }
511
- return true;
512
- case 'date': {
513
- // Handle both timestamp numbers and date strings
514
- let cellDate;
515
- if (typeof cellValue === 'number') {
516
- // Apply the same year transformation as in the format function
517
- const originalDate = new Date(cellValue);
518
- const currentYear = new Date().getFullYear();
519
- cellDate = new Date(currentYear, originalDate.getMonth(), originalDate.getDate());
520
- }
521
- else {
522
- cellDate = new Date(String(cellValue));
523
- }
524
- const filterDate = new Date(String(value));
525
- return cellDate.toDateString() === filterDate.toDateString();
526
- }
527
- case 'dateRange': {
528
- const startValue = filter.startValue;
529
- const endValue = filter.endValue;
530
- if (!startValue && !endValue)
531
- return true;
532
- // Handle both timestamp numbers and date strings
533
- let cellDateRange;
534
- if (typeof cellValue === 'number') {
535
- // Apply the same year transformation as in the format function
536
- const originalDate = new Date(cellValue);
537
- const currentYear = new Date().getFullYear();
538
- cellDateRange = new Date(currentYear, originalDate.getMonth(), originalDate.getDate());
539
- }
540
- else {
541
- cellDateRange = new Date(String(cellValue));
542
- }
543
- if (startValue && cellDateRange < new Date(String(startValue)))
544
- return false;
545
- if (endValue && cellDateRange > new Date(String(endValue)))
546
- return false;
547
- return true;
548
- }
549
- default:
550
- return true;
551
- }
552
- };
553
- const setFilter = (colIndex, filter) => {
554
- if (!filter.value && !filter.startValue && !filter.endValue) {
555
- // Remove filter if empty
556
- delete filterState.value[colIndex];
557
- }
558
- else {
559
- filterState.value[colIndex] = filter;
560
- }
561
- };
562
- const clearFilter = (colIndex) => {
563
- delete filterState.value[colIndex];
564
- };
565
- // Row action methods
566
- /**
567
- * Add a new row to the table.
568
- * @param rowData - Optional partial row data to initialize the new row with
569
- * @param position - Where to insert the row: 'start', 'end', or a specific index
570
- * @returns The index of the newly added row
571
- */
572
- const addRow = (rowData, position = 'end') => {
573
- // Create a new row with default empty values for each column
574
- const newRow = {};
575
- for (const column of columns.value) {
576
- newRow[column.name] = '';
577
- }
578
- // Merge in any provided row data
579
- if (rowData) {
580
- Object.assign(newRow, rowData);
581
- }
582
- let insertIndex;
583
- if (position === 'start') {
584
- insertIndex = 0;
585
- rows.value.unshift(newRow);
586
- }
587
- else if (position === 'end') {
588
- insertIndex = rows.value.length;
589
- rows.value.push(newRow);
590
- }
591
- else {
592
- insertIndex = Math.max(0, Math.min(position, rows.value.length));
593
- rows.value.splice(insertIndex, 0, newRow);
594
- }
595
- return insertIndex;
596
- };
597
- /**
598
- * Delete a row from the table.
599
- * @param rowIndex - The index of the row to delete
600
- * @returns The deleted row, or null if the index was invalid
601
- */
602
- const deleteRow = (rowIndex) => {
603
- if (rowIndex < 0 || rowIndex >= rows.value.length) {
604
- return null;
605
- }
606
- const [deletedRow] = rows.value.splice(rowIndex, 1);
607
- // Clean up row modifications tracking for the deleted row
608
- delete rowModifications.value[rowIndex];
609
- delete rowExpandStates.value[rowIndex];
610
- // Shift modification/expand state indices for rows after the deleted one
611
- const newModifications = {};
612
- const newExpandStates = {};
613
- for (const [key, value] of Object.entries(rowModifications.value)) {
614
- const idx = parseInt(key);
615
- if (idx > rowIndex) {
616
- newModifications[idx - 1] = value;
617
- }
618
- else {
619
- newModifications[idx] = value;
620
- }
621
- }
622
- for (const [key, value] of Object.entries(rowExpandStates.value)) {
623
- const idx = parseInt(key);
624
- if (idx > rowIndex) {
625
- newExpandStates[idx - 1] = value;
626
- }
627
- else {
628
- newExpandStates[idx] = value;
629
- }
630
- }
631
- rowModifications.value = newModifications;
632
- rowExpandStates.value = newExpandStates;
633
- return deletedRow;
634
- };
635
- /**
636
- * Duplicate a row in the table.
637
- * @param rowIndex - The index of the row to duplicate
638
- * @returns The index of the new duplicated row, or -1 if the index was invalid
639
- */
640
- const duplicateRow = (rowIndex) => {
641
- if (rowIndex < 0 || rowIndex >= rows.value.length) {
642
- return -1;
643
- }
644
- // Deep clone the row data
645
- const originalRow = rows.value[rowIndex];
646
- const duplicatedRow = JSON.parse(JSON.stringify(originalRow));
647
- // Insert the duplicated row after the original
648
- const newIndex = rowIndex + 1;
649
- rows.value.splice(newIndex, 0, duplicatedRow);
650
- return newIndex;
651
- };
652
- /**
653
- * Insert a new row above the specified row.
654
- * @param rowIndex - The index of the row to insert above
655
- * @param rowData - Optional partial row data to initialize the new row with
656
- * @returns The index of the newly inserted row
657
- */
658
- const insertRowAbove = (rowIndex, rowData) => {
659
- const insertIndex = Math.max(0, rowIndex);
660
- return addRow(rowData, insertIndex);
661
- };
662
- /**
663
- * Insert a new row below the specified row.
664
- * @param rowIndex - The index of the row to insert below
665
- * @param rowData - Optional partial row data to initialize the new row with
666
- * @returns The index of the newly inserted row
667
- */
668
- const insertRowBelow = (rowIndex, rowData) => {
669
- const insertIndex = Math.min(rowIndex + 1, rows.value.length);
670
- return addRow(rowData, insertIndex);
671
- };
672
- /**
673
- * Move a row from one position to another.
674
- * @param fromIndex - The current index of the row to move
675
- * @param toIndex - The target index to move the row to
676
- * @returns true if the move was successful, false otherwise
677
- */
678
- const moveRow = (fromIndex, toIndex) => {
679
- // Validate indices
680
- if (fromIndex < 0 ||
681
- fromIndex >= rows.value.length ||
682
- toIndex < 0 ||
683
- toIndex >= rows.value.length ||
684
- fromIndex === toIndex) {
685
- return false;
686
- }
687
- // Remove the row from its current position
688
- const [movedRow] = rows.value.splice(fromIndex, 1);
689
- // Insert at the new position
690
- rows.value.splice(toIndex, 0, movedRow);
691
- // Update row modification and expand state indices
692
- const newModifications = {};
693
- const newExpandStates = {};
694
- for (const [key, value] of Object.entries(rowModifications.value)) {
695
- const idx = parseInt(key);
696
- let newIdx = idx;
697
- if (idx === fromIndex) {
698
- // The moved row
699
- newIdx = toIndex;
700
- }
701
- else if (fromIndex < toIndex) {
702
- // Moving down: rows between fromIndex and toIndex shift up
703
- if (idx > fromIndex && idx <= toIndex) {
704
- newIdx = idx - 1;
705
- }
706
- }
707
- else {
708
- // Moving up: rows between toIndex and fromIndex shift down
709
- if (idx >= toIndex && idx < fromIndex) {
710
- newIdx = idx + 1;
711
- }
712
- }
713
- newModifications[newIdx] = value;
714
- }
715
- for (const [key, value] of Object.entries(rowExpandStates.value)) {
716
- const idx = parseInt(key);
717
- let newIdx = idx;
718
- if (idx === fromIndex) {
719
- newIdx = toIndex;
720
- }
721
- else if (fromIndex < toIndex) {
722
- if (idx > fromIndex && idx <= toIndex) {
723
- newIdx = idx - 1;
724
- }
725
- }
726
- else {
727
- if (idx >= toIndex && idx < fromIndex) {
728
- newIdx = idx + 1;
729
- }
730
- }
731
- newExpandStates[newIdx] = value;
732
- }
733
- rowModifications.value = newModifications;
734
- rowExpandStates.value = newExpandStates;
735
- return true;
736
- };
737
- return {
738
- // state
739
- columns,
740
- config,
741
- connectionHandles,
742
- connectionPaths,
743
- display,
744
- filterState,
745
- ganttBars,
746
- modal,
747
- rows,
748
- sortState,
749
- table,
750
- updates,
751
- // getters
752
- filteredRows,
753
- hasPinnedColumns,
754
- isGanttView,
755
- isTreeView,
756
- isDependencyGraphEnabled,
757
- numberedRowWidth,
758
- zeroColumn,
759
- // actions
760
- addRow,
761
- clearFilter,
762
- closeModal,
763
- createConnection,
764
- deleteConnection,
765
- deleteRow,
766
- duplicateRow,
767
- getCellData,
768
- getCellDisplayValue,
769
- getConnectionsForBar,
770
- getFormattedValue,
771
- getHandlesForBar,
772
- getHeaderCellStyle,
773
- getIndent,
774
- getRowExpandSymbol,
775
- insertRowAbove,
776
- insertRowBelow,
777
- isRowGantt,
778
- isRowVisible,
779
- moveRow,
780
- registerConnectionHandle,
781
- registerGanttBar,
782
- resizeColumn,
783
- setCellData,
784
- setCellText,
785
- setFilter,
786
- sortByColumn,
787
- toggleRowExpand,
788
- unregisterConnectionHandle,
789
- unregisterGanttBar,
790
- updateGanttBar,
791
- updateRows,
792
- };
793
- });
794
- return createStore();
795
- };