-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmagicTable.html
More file actions
621 lines (530 loc) · 26.3 KB
/
magicTable.html
File metadata and controls
621 lines (530 loc) · 26.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
<!-- v2.3 with Exact Match Filter + Auto Totals Row -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MagicTable</title>
<link href="https://minisoft.it/icons/fix.png" rel="shortcut icon" type="image/x-icon" />
<link href="https://minisoft.it/icons/fix.png" rel="apple-touch-icon" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#006E81',
'primary-dark': '#006E81',
secondary: '#814000',
}
}
}
}
</script>
<style>
.sort-asc::after {
content: ' ↑';
color: white;
}
.sort-desc::after {
content: ' ↓';
color: white;
}
.sortable:not(.sort-asc):not(.sort-desc)::after {
content: ' ↕';
color: white;
}
</style>
</head>
<body class="bg-gray-100 flex flex-col min-h-screen">
<header class="bg-white border-b border-gray-200">
<div class="container mx-auto px-4 py-6 max-w-4xl">
<h1 class="text-3xl font-bold text-center text-gray-800">Magic_Table</h1>
<p class="text-gray-600 text-center mt-1">Interactive HTML Table Processor</p>
</div>
</header>
<main class="container mx-auto px-4 py-8 max-w-4xl">
<!-- Instructions Section -->
<div class="bg-white rounded-lg shadow-md p-6 mb-8">
<h2 class="text-xl font-semibold text-gray-800 mb-4">Instructions</h2>
<div class="bg-slate-50 border-l-4 border-primary p-4 rounded-r">
<ol class="list-decimal pl-5 space-y-1">
<li>Paste your HTML table in the text area provided</li>
<li>Click "Analyze Table" to identify column headers</li>
<li>Select which columns you want to include in the processed table</li>
<li>Click "Generate Table" to create your interactive table</li>
<li>Use the advanced filtering options to create complex filter rules</li>
<li>Click on column headers to sort the table by that column</li>
</ol>
</div>
</div>
<!-- Input Form -->
<div class="bg-white rounded-lg shadow-md p-6 mb-8">
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Paste your HTML table here</label>
<textarea id="tableInput" class="w-full h-40 p-3 border border-gray-300 rounded-lg" required></textarea>
</div>
<button id="analyzeButton" class="bg-primary text-white px-4 py-2 rounded-lg hover:bg-primary-dark transition duration-200">Analyze Table</button>
</div>
</div>
<!-- Column Selection -->
<div id="columnSelection" class="bg-white rounded-lg shadow-md p-6 mb-8 hidden">
<h2 class="text-xl font-semibold text-gray-800 mb-4">Select Columns to Include</h2>
<div class="space-y-4">
<div id="columnCheckboxes" class="space-y-2"></div>
<div class="flex gap-4">
<button id="selectAll" class="text-sm text-primary hover:text-primary-dark transition duration-200">Select All</button>
<button id="deselectAll" class="text-sm text-primary hover:text-primary-dark transition duration-200">Deselect All</button>
</div>
<button id="processButton" class="bg-primary text-white px-4 py-2 rounded-lg hover:bg-primary-dark transition duration-200">Generate Table</button>
</div>
</div>
<!-- Results Section -->
<div id="results" class="bg-white rounded-lg shadow-md p-6 hidden">
<h2 class="text-xl font-semibold text-gray-800 mb-4">Results</h2>
<div class="space-y-4 mb-4">
<div class="text-sm text-gray-600">
Showing <span id="filteredCount">0</span> of <span id="totalCount">0</span> rows
</div>
<!-- Advanced Filter Section -->
<div class="border border-gray-200 rounded-lg p-4 bg-gray-50">
<h3 class="text-sm font-semibold text-gray-700 mb-3">Advanced Filters</h3>
<div id="filterRules" class="space-y-2"></div>
<div class="mt-3 flex gap-2">
<button id="addFilter" class="text-sm bg-primary text-white px-3 py-1 rounded hover:bg-primary-dark transition duration-200">+ Add Filter</button>
<button id="clearFilters" class="text-sm text-primary hover:text-primary-dark transition duration-200 px-3 py-1">Clear All</button>
</div>
</div>
</div>
<div class="overflow-x-auto">
<table id="dataTable" class="min-w-full divide-y divide-gray-200">
<thead id="tableHead" class="bg-primary">
<!-- Headers will be added here by JavaScript -->
</thead>
<tbody id="tableBody" class="bg-white divide-y divide-gray-200">
<!-- Table data will be added here by JavaScript -->
</tbody>
<tfoot id="tableFoot" class="bg-gray-100 font-semibold">
<!-- Totals row will be added here by JavaScript -->
</tfoot>
</table>
</div>
</div>
</main>
<footer class="bg-white border-t border-gray-200 mt-auto">
<div class="container mx-auto px-4 py-6 max-w-4xl">
<p class="text-gray-600 text-center">© <a href="https://minisoft.it/" class="text-primary hover:text-primary-dark">Minisoft</a> — All rights reserved</p>
</div>
</footer>
<script>
$(document).ready(function() {
let extractedData = [];
let filteredData = [];
let selectedColumns = [];
let currentSort = { column: -1, direction: 'asc' };
let filters = [];
// Load sample HTML table data
const sampleTableHtml = `<table>
<tr>
<th>Company</th>
<th>Contact</th>
<th>Country</th>
</tr>
<tr>
<td>Alfreds Futterkiste</td>
<td>Maria Anders</td>
<td>Germany</td>
</tr>
<tr>
<td>Centro comercial Moctezuma</td>
<td>Francisco Chang</td>
<td>Mexico</td>
</tr>
<tr>
<td>Ernst Handel</td>
<td>Roland Mendel</td>
<td>Austria</td>
</tr>
<tr>
<td>Island Trading</td>
<td>Helen Bennett</td>
<td>UK</td>
</tr>
<tr>
<td>Laughing Bacchus Winecellars</td>
<td>Yoshi Tannamuri</td>
<td>Canada</td>
</tr>
<tr>
<td>Magazzini Alimentari Riuniti</td>
<td>Giovanni Rovelli</td>
<td>Italy</td>
</tr>
</table>`;
$('#tableInput').val(sampleTableHtml);
$('#analyzeButton').on('click', function() {
const tableHtml = $('#tableInput').val();
// Create a temporary div to parse the HTML
const temp = $('<div>').html(tableHtml);
const originalTable = $(temp).find('table').first();
if (originalTable.length === 0) {
alert('No valid table found in input');
return;
}
// Extract data from HTML table into clean array structure
extractedData = [];
const headers = [];
// Extract headers
originalTable.find('th').each(function() {
headers.push($(this).text().trim());
});
// If no th elements, try first tr with td elements
if (headers.length === 0) {
originalTable.find('tr:first td').each(function() {
headers.push($(this).text().trim());
});
extractedData.push(headers);
} else {
extractedData.push(headers);
}
// Extract data rows
const startRow = headers.length === 0 ? 1 : 0;
originalTable.find('tr').each(function(rowIndex) {
if (rowIndex < startRow) return;
const rowData = [];
$(this).find('td').each(function() {
rowData.push($(this).text().trim());
});
if (rowData.length > 0) {
extractedData.push(rowData);
}
});
// Create column selection checkboxes
const checkboxesDiv = $('#columnCheckboxes').empty();
extractedData[0].forEach((header, index) => {
const checkbox = $('<div>').addClass('flex items-center gap-2')
.append(
$('<input>')
.attr('type', 'checkbox')
.attr('id', `col-${index}`)
.attr('checked', true)
.addClass('h-4 w-4 text-primary border-gray-300 rounded')
)
.append(
$('<label>')
.attr('for', `col-${index}`)
.text(header || `Column ${index + 1}`)
.addClass('text-sm text-gray-700')
);
checkboxesDiv.append(checkbox);
});
$('#columnSelection').removeClass('hidden');
$('#results').addClass('hidden');
});
$('#selectAll').on('click', function() {
$('#columnCheckboxes input[type="checkbox"]').prop('checked', true);
});
$('#deselectAll').on('click', function() {
$('#columnCheckboxes input[type="checkbox"]').prop('checked', false);
});
$('#processButton').on('click', function() {
selectedColumns = [];
$('#columnCheckboxes input[type="checkbox"]').each(function(index) {
if ($(this).is(':checked')) {
selectedColumns.push(index);
}
});
if (selectedColumns.length === 0) {
alert('Please select at least one column');
return;
}
// Filter data to include only selected columns
filteredData = extractedData.map(row =>
selectedColumns.map(colIndex => row[colIndex] || '')
);
filters = [];
renderFilters();
renderTable();
$('#results').removeClass('hidden');
});
function renderTable() {
const tableHead = $('#tableHead');
const tableBody = $('#tableBody');
if (filteredData.length === 0) {
tableHead.html('');
tableBody.html('<tr><td colspan="100%" class="px-6 py-4 text-center text-gray-500 italic">No data to display</td></tr>');
return;
}
// Create header
const headers = filteredData[0];
tableHead.html('');
const headerRow = $('<tr>');
headers.forEach((header, index) => {
const th = $('<th>')
.addClass('px-6 py-3 text-left text-xs font-medium text-white uppercase tracking-wider whitespace-nowrap cursor-pointer sortable')
.text(header || `Column ${index + 1}`)
.on('click', () => sortTable(index));
headerRow.append(th);
});
tableHead.append(headerRow);
// Create body
tableBody.html('');
const dataRows = filteredData.slice(1);
dataRows.forEach((row, rowIndex) => {
const tr = $('<tr>').addClass('hover:bg-blue-50 transition-colors duration-150');
row.forEach(cell => {
const td = $('<td>')
.addClass('px-6 py-4 whitespace-nowrap text-sm text-gray-900')
.text(cell);
tr.append(td);
});
tableBody.append(tr);
});
// Apply filters after rendering
applyFilters();
// Small delay to ensure DOM is ready before counting
setTimeout(function() {
updateCounts();
updateTotals();
}, 10);
}
function sortTable(columnIndex) {
if (currentSort.column === columnIndex) {
currentSort.direction = currentSort.direction === 'asc' ? 'desc' : 'asc';
} else {
currentSort.column = columnIndex;
currentSort.direction = 'asc';
}
const header = filteredData[0];
const dataRows = filteredData.slice(1);
dataRows.sort((a, b) => {
const aVal = a[columnIndex] || '';
const bVal = b[columnIndex] || '';
// Try to compare as numbers first
const aNum = parseFloat(aVal);
const bNum = parseFloat(bVal);
if (!isNaN(aNum) && !isNaN(bNum)) {
return currentSort.direction === 'asc' ? aNum - bNum : bNum - aNum;
}
// Compare as strings
const comparison = aVal.localeCompare(bVal, undefined, { numeric: true, sensitivity: 'base' });
return currentSort.direction === 'asc' ? comparison : -comparison;
});
filteredData = [header, ...dataRows];
renderTable();
// Reapply sort class after table is rendered
$('#tableHead th').removeClass('sort-asc sort-desc');
$('#tableHead th').eq(columnIndex).addClass(currentSort.direction === 'asc' ? 'sort-asc' : 'sort-desc');
}
$('#addFilter').on('click', function() {
const newFilter = {
id: Date.now(),
column: 0,
condition: 'contains',
value: '',
operator: filters.length > 0 ? 'AND' : null
};
filters.push(newFilter);
renderFilters();
});
$('#clearFilters').on('click', function() {
filters = [];
renderFilters();
applyFilters();
});
function renderFilters() {
const container = $('#filterRules');
container.empty();
filters.forEach((filter, index) => {
const filterRow = $('<div>').addClass('flex flex-wrap gap-2 items-center p-2 bg-white rounded border border-gray-200');
// AND/OR operator (except for first filter)
if (index > 0) {
const operatorSelect = $('<select>')
.addClass('text-sm border border-gray-300 rounded px-2 py-1')
.html('<option value="AND">AND</option><option value="OR">OR</option>')
.val(filter.operator)
.on('change', function() {
filter.operator = $(this).val();
applyFilters();
});
filterRow.append(operatorSelect);
}
// Column select
const columnSelect = $('<select>')
.addClass('text-sm border border-gray-300 rounded px-2 py-1');
const headers = filteredData[0];
headers.forEach((header, i) => {
columnSelect.append(`<option value="${i}">${header || 'Column ' + (i + 1)}</option>`);
});
columnSelect.val(filter.column)
.on('change', function() {
filter.column = parseInt($(this).val());
applyFilters();
});
filterRow.append(columnSelect);
// Condition select
const conditionSelect = $('<select>')
.addClass('text-sm border border-gray-300 rounded px-2 py-1')
.html('<option value="contains">Contains</option><option value="not-contains">Does not contain</option><option value="exact">Exact match</option><option value="not-exact">Not exact match</option>')
.val(filter.condition)
.on('change', function() {
filter.condition = $(this).val();
applyFilters();
});
filterRow.append(conditionSelect);
// Value input
const valueInput = $('<input>')
.attr('type', 'text')
.attr('placeholder', 'Value...')
.addClass('text-sm border border-gray-300 rounded px-2 py-1 flex-1 min-w-[120px]')
.val(filter.value)
.on('input', function() {
filter.value = $(this).val();
applyFilters();
});
filterRow.append(valueInput);
// Remove button
const removeBtn = $('<button>')
.text('×')
.addClass('text-red-600 hover:text-red-800 font-bold text-lg px-2')
.on('click', function() {
filters.splice(index, 1);
renderFilters();
applyFilters();
});
filterRow.append(removeBtn);
container.append(filterRow);
});
}
function applyFilters() {
$('#dataTable tbody tr').each(function() {
const row = $(this);
const show = evaluateFilters(row);
row.toggle(show);
});
updateCounts();
updateTotals();
}
function evaluateFilters(row) {
if (filters.length === 0) return true;
let result = evaluateFilter(filters[0], row);
for (let i = 1; i < filters.length; i++) {
const filter = filters[i];
const filterResult = evaluateFilter(filter, row);
if (filter.operator === 'AND') {
result = result && filterResult;
} else {
result = result || filterResult;
}
}
return result;
}
function evaluateFilter(filter, row) {
const cellText = $(row).find('td').eq(filter.column).text().trim();
const filterValue = filter.value.trim();
// Case-insensitive comparison for all conditions
const cellTextLower = cellText.toLowerCase();
const filterValueLower = filterValue.toLowerCase();
switch (filter.condition) {
case 'contains':
return cellTextLower.includes(filterValueLower);
case 'not-contains':
return !cellTextLower.includes(filterValueLower);
case 'exact':
return cellTextLower === filterValueLower;
case 'not-exact':
return cellTextLower !== filterValueLower;
default:
return true;
}
}
function updateCounts() {
const total = $('#dataTable tbody tr').length;
const visible = $('#dataTable tbody tr:visible').length;
$('#totalCount').text(total);
$('#filteredCount').text(visible);
}
// Parse numeric value from text (handles currency, European/US formats)
function parseNumericValue(text) {
if (!text || typeof text !== 'string') return null;
// Remove common currency symbols and whitespace
let cleaned = text.replace(/[€$£¥₹₽\s]/g, '').trim();
// Remove thousand separators and handle decimal separators
// Detect format: if comma is followed by exactly 2 digits at end, it's European decimal
const europeanDecimal = /,\d{2}$/.test(cleaned);
if (europeanDecimal) {
// European format: 1.234,56 -> 1234.56
cleaned = cleaned.replace(/\./g, '').replace(',', '.');
} else {
// US format or no decimals: 1,234.56 -> 1234.56
cleaned = cleaned.replace(/,/g, '');
}
const num = parseFloat(cleaned);
return isNaN(num) ? null : num;
}
// Format number for display (matches input format style)
function formatNumber(num, useEuropeanFormat) {
if (useEuropeanFormat) {
return num.toLocaleString('de-DE', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
return num.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
// Detect if column uses European format
function detectEuropeanFormat(values) {
return values.some(v => /,\d{2}$/.test(v.replace(/[€$£¥₹₽\s]/g, '').trim()));
}
function updateTotals() {
const tableFoot = $('#tableFoot');
tableFoot.html('');
if (filteredData.length <= 1) return;
const headers = filteredData[0];
const visibleRows = $('#dataTable tbody tr:visible');
if (visibleRows.length === 0) return;
const totalsRow = $('<tr>');
const columnTotals = [];
const columnHasNumbers = [];
const columnFormats = [];
// Initialize arrays
headers.forEach(() => {
columnTotals.push(0);
columnHasNumbers.push(false);
columnFormats.push([]);
});
// Calculate totals from visible rows
visibleRows.each(function() {
$(this).find('td').each(function(colIndex) {
const text = $(this).text().trim();
const num = parseNumericValue(text);
if (num !== null) {
columnTotals[colIndex] += num;
columnHasNumbers[colIndex] = true;
columnFormats[colIndex].push(text);
}
});
});
// Build totals row
headers.forEach((header, index) => {
const td = $('<td>')
.addClass('px-6 py-3 whitespace-nowrap text-sm text-gray-900 border-t-2 border-gray-300');
if (columnHasNumbers[index]) {
const useEuropean = detectEuropeanFormat(columnFormats[index]);
const formattedTotal = formatNumber(columnTotals[index], useEuropean);
// Check if original values had currency symbols
const hasCurrency = columnFormats[index].some(v => /[€$£¥₹₽]/.test(v));
const currencySymbol = hasCurrency ? (columnFormats[index].find(v => /[€$£¥₹₽]/.test(v))?.match(/[€$£¥₹₽]/)?.[0] || '') : '';
td.html(`<strong>${formattedTotal}${currencySymbol ? ' ' + currencySymbol : ''}</strong>`);
} else if (index === 0) {
td.html('<strong>Totals</strong>');
} else {
td.text('—');
}
totalsRow.append(td);
});
tableFoot.append(totalsRow);
}
});
</script>
</body>
</html>