Skip to content

Commit a90fdf5

Browse files
committed
Added the work in progress
1 parent d09e187 commit a90fdf5

File tree

3 files changed

+372
-1
lines changed

3 files changed

+372
-1
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,5 @@ Still has some cleaning up to do, but usable datatables.
1818
- option to use modal for editing column
1919
- more testing
2020
- switch to use pico dropdowns for column toggling
21-
- when editing a date / time column use browser datetime input type
21+
- when editing a date / time column use browser datetime input type
22+
- When row is edited we cant edit that row again - becasue event listeners arent attached anymore

index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ <h1>DataTables Viewer</h1>
8989
<div id="tableContainer"></div>
9090
</main>
9191
<script src="src/DataTableViewer.js"></script>
92+
9293
<script>
9394
// Sample data for preview - replace with your actual data
9495
const sampleData = [{ id: 1, name: "John Doe", date: "2024-01-01", status: "Active" },

src/DataTableViewer-WIP.js

Lines changed: 369 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,369 @@
1+
class DataTableViewer {
2+
constructor(config) {
3+
this.config = {
4+
dataUrl: config.dataUrl,
5+
editEndpoint: config.editEndpoint || '',
6+
extraEditData: config.extraEditData || {},
7+
onEditSuccess: config.onEditSuccess || ((response) => console.log('Edit success:', response)),
8+
columns: config.columns || [],
9+
rowsPerPage: config.rowsPerPage || 10,
10+
container: config.container || document.getElementById('tableContainer')
11+
};
12+
13+
this.data = [];
14+
this.filteredData = [];
15+
this.currentPage = 1;
16+
this.sortColumn = null;
17+
this.sortDirection = 'asc';
18+
this.columnSearchValues = {};
19+
this.globalSearchValue = '';
20+
this.hiddenColumns = new Set();
21+
22+
this.init();
23+
}
24+
25+
async init() {
26+
await this.loadData();
27+
this.setupControls();
28+
this.render();
29+
}
30+
31+
async loadData() {
32+
this.config.container.innerHTML = '<div class="loading">Loading data...</div>';
33+
try {
34+
if (this.config.dataUrl) {
35+
const response = await fetch(this.config.dataUrl);
36+
this.data = await response.json();
37+
} else {
38+
// Use sample data for preview
39+
this.data = sampleData;
40+
}
41+
this.filteredData = [...this.data];
42+
} catch (error) {
43+
console.error('Error loading data:', error);
44+
this.config.container.innerHTML = '<div class="error">Error loading data</div>';
45+
}
46+
}
47+
48+
setupControls() {
49+
// Global search
50+
document.getElementById('globalSearch').addEventListener('input', (e) => {
51+
this.globalSearchValue = e.target.value.toLowerCase();
52+
this.filterData();
53+
this.currentPage = 1;
54+
this.render();
55+
});
56+
57+
// Rows per page
58+
document.getElementById('rowsPerPage').addEventListener('change', (e) => {
59+
this.config.rowsPerPage = parseInt(e.target.value);
60+
this.currentPage = 1;
61+
this.render();
62+
});
63+
64+
// Column toggle menu
65+
const toggleBtn = document.getElementById('toggleColumnsBtn');
66+
const menu = document.getElementById('columnToggleMenu');
67+
68+
toggleBtn.addEventListener('click', () => {
69+
menu.classList.toggle('hidden');
70+
menu.style.top = `${toggleBtn.offsetTop + toggleBtn.offsetHeight}px`;
71+
menu.style.left = `${toggleBtn.offsetLeft}px`;
72+
73+
menu.innerHTML = this.config.columns.map(col => `
74+
<label>
75+
<input type="checkbox"
76+
${!this.hiddenColumns.has(col.field) ? 'checked' : ''}
77+
data-column="${col.field}">
78+
${col.title}
79+
</label>
80+
`).join('');
81+
});
82+
83+
menu.addEventListener('change', (e) => {
84+
if (e.target.matches('input[type="checkbox"]')) {
85+
const column = e.target.dataset.column;
86+
if (e.target.checked) {
87+
this.hiddenColumns.delete(column);
88+
} else {
89+
this.hiddenColumns.add(column);
90+
}
91+
this.render();
92+
}
93+
});
94+
95+
// Close menu when clicking outside
96+
document.addEventListener('click', (e) => {
97+
if (!menu.contains(e.target) && e.target !== toggleBtn) {
98+
menu.classList.add('hidden');
99+
}
100+
});
101+
}
102+
filterData() {
103+
this.filteredData = this.data.filter(row => {
104+
// Global search
105+
if (this.globalSearchValue) {
106+
const rowString = Object.values(row).join(' ').toLowerCase();
107+
if (!rowString.includes(this.globalSearchValue)) return false;
108+
}
109+
110+
// Column-specific search
111+
return Object.entries(this.columnSearchValues).every(([field, searchValue]) => {
112+
if (!searchValue) return true;
113+
const value = String(row[field]).toLowerCase();
114+
// For select type columns, do exact match
115+
const col = this.config.columns.find(c => c.field === field);
116+
if (col?.searchType === 'select') {
117+
return value === searchValue.toLowerCase();
118+
}
119+
// For text type columns, do contains match
120+
return value.includes(searchValue.toLowerCase());
121+
});
122+
});
123+
}
124+
125+
sortData(column) {
126+
if (this.sortColumn === column) {
127+
this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
128+
} else {
129+
this.sortColumn = column;
130+
this.sortDirection = 'asc';
131+
}
132+
133+
const col = this.config.columns.find(c => c.field === column);
134+
135+
this.filteredData.sort((a, b) => {
136+
let valA = a[column];
137+
let valB = b[column];
138+
139+
if (col.type === 'date') {
140+
valA = new Date(valA);
141+
valB = new Date(valB);
142+
}
143+
144+
if (valA < valB) return this.sortDirection === 'desc' ? 1 : -1;
145+
if (valA > valB) return this.sortDirection === 'desc' ? -1 : 1;
146+
return 0;
147+
});
148+
149+
this.render();
150+
}
151+
152+
async handleEdit(rowIndex, row) {
153+
try {
154+
const response = await fetch(this.config.editEndpoint, {
155+
method: 'POST',
156+
headers: {
157+
'Content-Type': 'application/json'
158+
},
159+
body: JSON.stringify({
160+
...row,
161+
...this.config.extraEditData
162+
})
163+
});
164+
165+
if (!response.ok) throw new Error('Edit failed');
166+
167+
const result = await response.json();
168+
this.config.onEditSuccess(result);
169+
170+
// Update local data
171+
Object.assign(this.data[rowIndex], row);
172+
this.filterData();
173+
this.render();
174+
175+
} catch (error) {
176+
console.error('Error saving edit:', error);
177+
alert('Failed to save changes');
178+
}
179+
}
180+
181+
renderHeader() {
182+
const visibleColumns = this.config.columns.filter(col => !this.hiddenColumns.has(col.field));
183+
return `
184+
<thead class="container-thead">
185+
<tr>
186+
<th>Edit</th>
187+
${visibleColumns.map(col => `
188+
<th class="header-cell ${this.sortColumn === col.field ? `sort-${this.sortDirection}` : 'sort-indicator'}"
189+
data-column="${col.field}">
190+
${col.title}
191+
</th>
192+
`).join('')}
193+
</tr>
194+
</thead>
195+
`;
196+
}
197+
198+
renderBody(ret = 'html') {
199+
const visibleColumns = this.config.columns.filter(col => !this.hiddenColumns.has(col.field));
200+
const startIndex = (this.currentPage - 1) * this.config.rowsPerPage;
201+
const endIndex = startIndex + this.config.rowsPerPage;
202+
const pageData = this.filteredData.slice(startIndex, endIndex);
203+
204+
const tbody = `
205+
<tbody class="container-tbody">
206+
${pageData.map((row, idx) => `
207+
<tr id="row-${idx}">
208+
<td>
209+
<button class="edit-btn" data-row="${idx}">Edit</button>
210+
</td>
211+
${visibleColumns.map(col => `
212+
<td class="data-cell" data-column="${col.field}">${row[col.field]}</td>
213+
`).join('')}
214+
</tr>
215+
`).join('')}
216+
</tbody>
217+
`;
218+
219+
if(ret === 'html'){
220+
return tbody;
221+
} else if(ret === 'update'){
222+
this.config.container.querySelector('.container-tbody').innerHTML = tbody;
223+
this.renderEdit();
224+
}
225+
}
226+
227+
renderFooter() {
228+
const visibleColumns = this.config.columns.filter(col => !this.hiddenColumns.has(col.field));
229+
return `
230+
<tfoot class="container-tfoot">
231+
<tr>
232+
<td></td>
233+
${visibleColumns.map(col => `
234+
<td>
235+
${col.searchType === 'select' ?
236+
`<select class="column-search" data-column="${col.field}">
237+
<option value="">All</option>
238+
${[...new Set(this.data.map(item => item[col.field]))].sort().map(value =>
239+
`<option value="${value}" ${this.columnSearchValues[col.field] === value ? 'selected' : ''}>${value}</option>`
240+
).join('')}
241+
</select>` :
242+
`<input type="text" class="column-search" data-column="${col.field}"
243+
value="${this.columnSearchValues[col.field] || ''}"
244+
placeholder="Search ${col.title}...">`
245+
}
246+
</td>
247+
`).join('')}
248+
</tr>
249+
</tfoot>
250+
`;
251+
}
252+
253+
renderPagination() {
254+
return `
255+
<nav class="container-pagination">
256+
<ul></ul>
257+
<ul><li>
258+
<div class="pagination">
259+
<button ${this.currentPage === 1 ? 'disabled' : ''} onclick="dataTable.currentPage--; dataTable.render()">Previous</button>
260+
<span>Page ${this.currentPage} of ${Math.ceil(this.filteredData.length / this.config.rowsPerPage)}</span>
261+
<button ${this.currentPage === Math.ceil(this.filteredData.length / this.config.rowsPerPage) ? 'disabled' : ''}
262+
onclick="dataTable.currentPage++; dataTable.render()">Next</button>
263+
</div>
264+
</li></ul>
265+
<ul></ul>
266+
</nav>
267+
`;
268+
}
269+
270+
render() {
271+
const startIndex = (this.currentPage - 1) * this.config.rowsPerPage;
272+
const endIndex = startIndex + this.config.rowsPerPage;
273+
const pageData = this.filteredData.slice(startIndex, endIndex);
274+
275+
this.config.container.innerHTML = `
276+
<table class="striped">
277+
${this.renderHeader()}
278+
${this.renderBody()}
279+
${this.renderFooter()}
280+
</table>
281+
${this.renderPagination()}
282+
`;
283+
284+
this.renderColumnSearch();
285+
this.renderColumnSort();
286+
this.renderEdit();
287+
}
288+
289+
renderColumnSearch() {
290+
// Setup column search
291+
this.config.container.querySelectorAll('.column-search').forEach(input => {
292+
input.addEventListener('keyup', (e) => {
293+
const column = e.target.dataset.column;
294+
const value = e.target.value;
295+
296+
if (value === '') {
297+
delete this.columnSearchValues[column];
298+
} else {
299+
this.columnSearchValues[column] = value;
300+
}
301+
this.filterData();
302+
this.currentPage = 1;
303+
this.render();
304+
//this.renderColumnSearch();
305+
// Focus the input and move the cursor to the end
306+
const targetInput = document.querySelector(`tfoot input[data-column="${column}"]`);
307+
if (targetInput) {
308+
targetInput.focus();
309+
const length = targetInput.value.length;
310+
targetInput.setSelectionRange(length, length);
311+
}
312+
});
313+
});
314+
}
315+
316+
renderColumnSort() {
317+
// Setup sorting
318+
this.config.container.querySelectorAll('.header-cell').forEach(header => {
319+
header.addEventListener('click', () => {
320+
const column = header.dataset.column;
321+
this.sortData(column);
322+
});
323+
});
324+
}
325+
326+
renderEdit() {
327+
// Setup edit buttons
328+
const visibleColumns = this.config.columns.filter(col => !this.hiddenColumns.has(col.field));
329+
const startIndex = (this.currentPage - 1) * this.config.rowsPerPage;
330+
const endIndex = startIndex + this.config.rowsPerPage;
331+
const pageData = this.filteredData.slice(startIndex, endIndex);
332+
333+
this.config.container.querySelectorAll('.edit-btn').forEach(btn => {
334+
btn.addEventListener('click', () => {
335+
const rowIndex = parseInt(btn.dataset.row);
336+
const row = pageData[rowIndex];
337+
338+
const tr = document.getElementById(`row-${rowIndex}`);
339+
const originalContent = tr.innerHTML;
340+
341+
tr.innerHTML = `
342+
<td>
343+
<button class="save-btn">Save</button>
344+
<button class="cancel-btn">Cancel</button>
345+
</td>
346+
${visibleColumns.map(col => `
347+
<td>
348+
<input type="text" value="${row[col.field]}" data-field="${col.field}">
349+
</td>
350+
`).join('')}
351+
`;
352+
353+
tr.querySelector('.save-btn').addEventListener('click', () => {
354+
const updatedRow = { ...row };
355+
tr.querySelectorAll('input[data-field]').forEach(input => {
356+
updatedRow[input.dataset.field] = input.value;
357+
});
358+
this.handleEdit(rowIndex, updatedRow);
359+
});
360+
361+
tr.querySelector('.cancel-btn').addEventListener('click', () => {
362+
//this.renderBody();
363+
tr.innerHTML = originalContent;
364+
});
365+
});
366+
});
367+
}
368+
369+
}

0 commit comments

Comments
 (0)