Skip to content

Commit 430a34b

Browse files
refactor: split dense grid.js UI module into cohesive sub-components
1 parent 5c99da5 commit 430a34b

8 files changed

Lines changed: 1238 additions & 1218 deletions

File tree

core/ui/modules/grid-actions.js

Lines changed: 463 additions & 0 deletions
Large diffs are not rendered by default.

core/ui/modules/grid-data.js

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import { state, persistState } from './state.js';
2+
import { backendApi } from './api.js';
3+
import { updateStatus, showLoading, showErrorState, updateToolbarButtons } from './ui.js';
4+
import { updatePagination, renderDataGrid } from './grid-render.js';
5+
6+
export async function loadTableColumns() {
7+
if (!state.selectedTable) return;
8+
9+
try {
10+
const columns = await backendApi.getTableInfo(state.selectedTable);
11+
state.tableColumns = columns.map(r => ({
12+
cid: r.ordinal,
13+
name: r.identifier,
14+
type: r.declaredType,
15+
notnull: r.isRequired,
16+
dflt_value: r.defaultExpression,
17+
isPrimaryKey: r.primaryKeyPosition > 0
18+
})).sort((a, b) => a.cid - b.cid);
19+
20+
// Sanitize state based on new columns
21+
const colNames = new Set(state.tableColumns.map(c => c.name));
22+
23+
// 1. Reset sort if column is gone
24+
if (state.sortedColumn && !colNames.has(state.sortedColumn)) {
25+
state.sortedColumn = null;
26+
state.sortAscending = true;
27+
}
28+
29+
// 2. Clear filters for deleted columns
30+
for (const col of Object.keys(state.columnFilters)) {
31+
if (!colNames.has(col)) {
32+
delete state.columnFilters[col];
33+
}
34+
}
35+
} catch (err) {
36+
console.error('Error loading columns:', err);
37+
updateStatus('Error loading columns');
38+
}
39+
}
40+
41+
export async function loadTableData(showSpinner = true, saveScrollPosition = true) {
42+
if (!state.selectedTable) return;
43+
44+
const container = document.getElementById('gridContainer');
45+
46+
// Only capture scroll position if the grid is currently visible (not loading/error state)
47+
// This prevents overwriting the saved position with 0 when reloading data while a spinner is shown.
48+
if (saveScrollPosition && container && container.querySelector('.data-grid')) {
49+
state.scrollPosition.left = container.scrollLeft;
50+
state.scrollPosition.top = container.scrollTop;
51+
}
52+
53+
if (showSpinner) {
54+
state.isLoadingData = true;
55+
showLoading();
56+
}
57+
updateToolbarButtons();
58+
59+
try {
60+
// Build query options
61+
const filters = [];
62+
// Column filters
63+
for (const [colName, filterValue] of Object.entries(state.columnFilters)) {
64+
if (filterValue && filterValue.trim()) {
65+
filters.push({ column: colName, value: filterValue });
66+
}
67+
}
68+
69+
const countOptions = {
70+
filters,
71+
globalFilter: state.filterQuery,
72+
columns: state.tableColumns.map(c => c.name) // Needed for global filter
73+
};
74+
75+
// Get total count
76+
state.totalRecordCount = await backendApi.fetchTableCount(state.selectedTable, countOptions);
77+
state.totalPageCount = Math.max(1, Math.ceil(state.totalRecordCount / state.rowsPerPage));
78+
79+
if (state.currentPageIndex >= state.totalPageCount) {
80+
state.currentPageIndex = Math.max(0, state.totalPageCount - 1);
81+
}
82+
83+
// Get data
84+
const isTable = state.selectedTableType === 'table';
85+
const columnNames = state.tableColumns.map(c => c.name);
86+
87+
// For tables, we need to explicitly request the 'rowid' column to handle row identification.
88+
// The frontend expects rowid at index 0 for tables (see `getRowId` and `getRowDataOffset`).
89+
// Query builder handles the construction: SELECT "rowid", "col1", ...
90+
const queryColumns = isTable ? ['rowid', ...columnNames] : columnNames;
91+
92+
const queryOptions = {
93+
columns: queryColumns,
94+
orderBy: state.sortedColumn,
95+
orderDir: state.sortAscending ? 'ASC' : 'DESC',
96+
limit: state.rowsPerPage,
97+
offset: state.currentPageIndex * state.rowsPerPage,
98+
filters,
99+
globalFilter: state.filterQuery
100+
};
101+
102+
const dataResult = await backendApi.fetchTableData(state.selectedTable, queryOptions);
103+
104+
state.gridData = dataResult.rows || [];
105+
106+
// If not showing spinner (background refresh), capture the current scroll position
107+
// right before rendering. This ensures we use the latest scroll position,
108+
// which covers cases where the user scrolled during fetch or if an edit operation
109+
// updated the view (and restored scroll) while the fetch was pending.
110+
if (!showSpinner && container && container.querySelector('.data-grid')) {
111+
state.scrollPosition.left = container.scrollLeft;
112+
state.scrollPosition.top = container.scrollTop;
113+
}
114+
115+
// Optimization: If editing, skip render to avoid destroying the active editor
116+
if (!showSpinner && state.editingCellInfo) {
117+
// We updated gridData, so the data is fresh.
118+
// We skip the DOM update to keep the <textarea> alive.
119+
// updateCellDom in edit.js handles the visual update of the modified cell.
120+
} else {
121+
renderDataGrid(state.scrollPosition.top, state.scrollPosition.left);
122+
}
123+
124+
if (container) {
125+
container.scrollLeft = state.scrollPosition.left;
126+
container.scrollTop = state.scrollPosition.top;
127+
}
128+
129+
updatePagination();
130+
updateStatus(`${state.totalRecordCount} records`);
131+
132+
} catch (err) {
133+
console.error('Error loading data:', err);
134+
updateStatus(`Error: ${err.message}`);
135+
showErrorState(err.message);
136+
} finally {
137+
if (showSpinner) {
138+
state.isLoadingData = false;
139+
}
140+
}
141+
}

core/ui/modules/grid-events.js

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
import { state, persistState } from './state.js';
2+
import {
3+
goToPage,
4+
onFilterChange,
5+
onPageSizeChange,
6+
onDateFormatChange,
7+
startColumnResize,
8+
onColumnFilterKeydown,
9+
applyColumnFilter,
10+
onColumnHeaderClick,
11+
toggleColumnPin,
12+
toggleRowPin,
13+
onSelectAllClick,
14+
onColumnSort,
15+
onRowNumberClick,
16+
onCellClick,
17+
onCellDoubleClick
18+
} from './grid-actions.js';
19+
import { openCellPreview } from './edit.js';
20+
21+
export function initGridControls() {
22+
document.getElementById('filterInput')?.addEventListener('keyup', onFilterChange);
23+
document.getElementById('pageSizeSelect')?.addEventListener('change', onPageSizeChange);
24+
document.getElementById('dateFormatSelect')?.addEventListener('change', onDateFormatChange);
25+
26+
document.getElementById('btnFirst')?.addEventListener('click', () => goToPage(0));
27+
document.getElementById('btnPrev')?.addEventListener('click', () => goToPage(state.currentPageIndex - 1));
28+
document.getElementById('btnNext')?.addEventListener('click', () => goToPage(state.currentPageIndex + 1));
29+
document.getElementById('btnLast')?.addEventListener('click', () => goToPage(state.totalPageCount - 1));
30+
}
31+
32+
export function initGridInteraction() {
33+
const container = document.getElementById('gridContainer');
34+
if (!container) return;
35+
36+
// --- Mousedown Delegation (Resize) ---
37+
container.addEventListener('mousedown', (event) => {
38+
if (event.target.classList.contains('resize-handle')) {
39+
event.stopPropagation();
40+
const headerCell = event.target.closest('.header-cell');
41+
if (headerCell && headerCell.dataset.column) {
42+
startColumnResize(event, headerCell.dataset.column);
43+
}
44+
}
45+
});
46+
47+
// --- Keydown Delegation (Filters) ---
48+
container.addEventListener('keydown', (event) => {
49+
if (event.target.classList.contains('column-filter')) {
50+
const colName = event.target.dataset.column;
51+
if (colName) onColumnFilterKeydown(event, colName);
52+
}
53+
});
54+
55+
// --- Click Delegation ---
56+
container.addEventListener('click', (event) => {
57+
const target = event.target;
58+
59+
// --- Header Interactions ---
60+
if (target.closest('.grid-header')) {
61+
// 1. Filter Apply Button
62+
if (target.closest('.filter-apply-btn')) {
63+
event.stopPropagation();
64+
const headerCell = target.closest('.header-cell');
65+
if (headerCell && headerCell.dataset.column) {
66+
applyColumnFilter(headerCell.dataset.column);
67+
}
68+
return;
69+
}
70+
71+
// 2. Prevent sort when clicking inputs/bottom area
72+
if (target.closest('.header-bottom') || target.closest('.column-filter')) {
73+
event.stopPropagation();
74+
return;
75+
}
76+
77+
// 3. Column Selection Icon
78+
if (target.closest('.select-column-icon')) {
79+
event.stopPropagation();
80+
const headerCell = target.closest('.header-cell');
81+
if (headerCell && headerCell.dataset.column) {
82+
onColumnHeaderClick(event, headerCell.dataset.column);
83+
}
84+
return;
85+
}
86+
87+
// 4. Header Pin Icon
88+
if (target.closest('.pin-icon')) {
89+
event.stopPropagation();
90+
const headerCell = target.closest('.header-cell');
91+
if (headerCell && headerCell.dataset.column) {
92+
toggleColumnPin(event, headerCell.dataset.column);
93+
}
94+
return;
95+
}
96+
97+
// 5. Select All (Row Number Header)
98+
if (target.closest('.row-number-header')) {
99+
onSelectAllClick(event);
100+
return;
101+
}
102+
103+
// 6. Sort (Header Top)
104+
const headerTop = target.closest('.header-top');
105+
if (headerTop) {
106+
const headerCell = headerTop.closest('.header-cell');
107+
if (headerCell && headerCell.dataset.column) {
108+
onColumnSort(headerCell.dataset.column);
109+
}
110+
return;
111+
}
112+
return;
113+
}
114+
115+
// --- Body Interactions ---
116+
117+
// 1. Row Pin Icon
118+
if (target.closest('.pin-icon')) {
119+
const rowEl = target.closest('.data-row');
120+
if (rowEl) {
121+
const rowId = rowEl.dataset.rowid;
122+
// Handle type conversion
123+
const safeRowId = resolveRowIdType(rowId);
124+
toggleRowPin(event, safeRowId);
125+
}
126+
return;
127+
}
128+
129+
// 2. Expand Icon
130+
if (target.closest('.expand-icon')) {
131+
const cellEl = target.closest('.data-cell');
132+
if (cellEl) {
133+
const rowIdx = parseInt(cellEl.dataset.rowidx, 10);
134+
const colIdx = parseInt(cellEl.dataset.colidx, 10);
135+
const rowId = resolveRowIdType(cellEl.closest('.data-row').dataset.rowid);
136+
openCellPreview(rowIdx, colIdx, rowId);
137+
}
138+
return;
139+
}
140+
141+
// 3. Row Number Cell
142+
if (target.closest('.row-number')) {
143+
const rowEl = target.closest('.data-row');
144+
if (rowEl) {
145+
const rowId = resolveRowIdType(rowEl.dataset.rowid);
146+
const rowIdx = parseInt(rowEl.dataset.rowidx, 10);
147+
onRowNumberClick(event, rowId, rowIdx);
148+
}
149+
return;
150+
}
151+
152+
// 4. Data Cell
153+
const cellEl = target.closest('.data-cell');
154+
if (cellEl) {
155+
const rowIdx = parseInt(cellEl.dataset.rowidx, 10);
156+
const colIdx = parseInt(cellEl.dataset.colidx, 10);
157+
const rowEl = cellEl.closest('.data-row');
158+
const rowId = resolveRowIdType(rowEl.dataset.rowid);
159+
onCellClick(event, rowIdx, colIdx, rowId);
160+
return;
161+
}
162+
163+
});
164+
165+
// Double Click Handler
166+
container.addEventListener('dblclick', (event) => {
167+
const cellEl = event.target.closest('.data-cell');
168+
if (cellEl && !cellEl.classList.contains('row-number')) {
169+
const rowIdx = parseInt(cellEl.dataset.rowidx, 10);
170+
const colIdx = parseInt(cellEl.dataset.colidx, 10);
171+
const rowEl = cellEl.closest('.data-row');
172+
const rowId = resolveRowIdType(rowEl.dataset.rowid);
173+
onCellDoubleClick(event, rowIdx, colIdx, rowId);
174+
}
175+
});
176+
177+
// Lazy overflow detection
178+
container.addEventListener('mouseover', (event) => {
179+
const cell = event.target.closest('.data-cell');
180+
if (cell && !cell.classList.contains('checked-overflow')) {
181+
const textSpan = cell.querySelector('.cell-text');
182+
if (textSpan) {
183+
const hasOverflow = textSpan.scrollWidth > textSpan.clientWidth;
184+
cell.classList.toggle('has-overflow', hasOverflow);
185+
cell.classList.add('checked-overflow');
186+
}
187+
}
188+
});
189+
190+
// Track scroll position for state persistence
191+
container.addEventListener('scroll', () => {
192+
state.scrollPosition.left = container.scrollLeft;
193+
state.scrollPosition.top = container.scrollTop;
194+
persistState();
195+
}, { passive: true });
196+
}
197+
198+
function resolveRowIdType(idStr) {
199+
if (idStr === undefined || idStr === null) return idStr;
200+
// Row IDs in SQLite are integers, but if using 'WITHOUT ROWID' tables, PK could be anything.
201+
// However, for standard tables, we try to keep them as numbers to match what the backend returns.
202+
const num = Number(idStr);
203+
return !isNaN(num) && idStr.trim() !== '' ? num : idStr;
204+
}

0 commit comments

Comments
 (0)