tabla-json / index.html
Yoleo's picture
Add 2 files
0563457 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Advanced Data Table</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
/* Custom styles */
.table-container {
overflow-x: auto;
max-width: 100%;
}
.table-wrapper {
position: relative;
}
.table-header {
position: sticky;
top: 0;
background-color: white;
z-index: 10;
}
.resize-handle {
position: absolute;
top: 0;
right: 0;
width: 5px;
height: 100%;
background-color: #e5e7eb;
cursor: col-resize;
}
.resize-handle:hover {
background-color: #3b82f6;
}
.draggable-header {
cursor: move;
}
.column-options {
position: absolute;
right: 0;
top: 100%;
z-index: 20;
background: white;
border: 1px solid #e5e7eb;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
min-width: 200px;
}
.filter-panel {
position: absolute;
right: 0;
top: 100%;
z-index: 20;
background: white;
border: 1px solid #e5e7eb;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
min-width: 250px;
padding: 1rem;
}
.ellipsis-text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 300px;
}
.tooltip {
position: absolute;
background: #333;
color: white;
padding: 5px 10px;
border-radius: 4px;
font-size: 14px;
z-index: 100;
display: none;
}
.sort-icon {
transition: transform 0.2s;
}
.sort-asc {
transform: rotate(180deg);
}
.file-drop-area {
border: 2px dashed #cbd5e1;
border-radius: 0.5rem;
padding: 2rem;
text-align: center;
cursor: pointer;
transition: all 0.3s;
}
.file-drop-area.active {
border-color: #3b82f6;
background-color: #eff6ff;
}
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1000;
justify-content: center;
align-items: center;
}
.modal-content {
background-color: white;
padding: 2rem;
border-radius: 0.5rem;
max-width: 90%;
max-height: 90%;
overflow: auto;
}
.column-menu {
position: absolute;
background: white;
border: 1px solid #e5e7eb;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
min-width: 180px;
z-index: 30;
}
</style>
</head>
<body class="bg-gray-50 p-4">
<div class="max-w-7xl mx-auto">
<h1 class="text-2xl font-bold text-gray-800 mb-6">Advanced Data Table</h1>
<!-- Controls Section -->
<div class="bg-white rounded-lg shadow p-4 mb-6">
<div class="flex flex-wrap gap-4 items-center justify-between mb-4">
<div class="flex gap-2">
<button id="uploadBtn" class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded flex items-center gap-2">
<i class="fas fa-upload"></i> Upload JSON
</button>
<button id="downloadBtn" class="bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded flex items-center gap-2">
<i class="fas fa-download"></i> Download Data
</button>
<button id="pasteBtn" class="bg-purple-500 hover:bg-purple-600 text-white px-4 py-2 rounded flex items-center gap-2">
<i class="fas fa-paste"></i> Paste from Clipboard
</button>
<button id="loadConfigBtn" class="bg-yellow-500 hover:bg-yellow-600 text-white px-4 py-2 rounded flex items-center gap-2">
<i class="fas fa-cog"></i> Load Config
</button>
<button id="saveConfigBtn" class="bg-indigo-500 hover:bg-indigo-600 text-white px-4 py-2 rounded flex items-center gap-2">
<i class="fas fa-save"></i> Save Config
</button>
</div>
<div class="flex gap-2">
<button id="toggleFiltersBtn" class="bg-gray-200 hover:bg-gray-300 text-gray-800 px-4 py-2 rounded flex items-center gap-2">
<i class="fas fa-filter"></i> Toggle Filters
</button>
<button id="toggleColumnsBtn" class="bg-gray-200 hover:bg-gray-300 text-gray-800 px-4 py-2 rounded flex items-center gap-2">
<i class="fas fa-columns"></i> Columns
</button>
</div>
</div>
<!-- File Drop Area -->
<div id="fileDropArea" class="file-drop-area mb-4">
<div class="flex flex-col items-center justify-center">
<i class="fas fa-cloud-upload-alt text-4xl text-gray-400 mb-2"></i>
<p class="text-gray-600">Drag & drop your JSON file here</p>
<p class="text-sm text-gray-500 mt-1">or click to browse files</p>
</div>
<input type="file" id="fileInput" accept=".json" class="hidden">
</div>
<!-- URL Input -->
<div class="flex gap-2 mb-4">
<input type="text" id="jsonUrl" placeholder="Enter JSON URL" class="flex-1 border border-gray-300 rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">
<button id="loadUrlBtn" class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded">
Load from URL
</button>
</div>
<!-- Search Input -->
<div class="relative mb-4">
<input type="text" id="globalSearch" placeholder="Search across all columns..." class="w-full border border-gray-300 rounded px-3 py-2 pl-10 focus:outline-none focus:ring-2 focus:ring-blue-500">
<i class="fas fa-search absolute left-3 top-3 text-gray-400"></i>
</div>
</div>
<!-- Filter Panel -->
<div id="filterPanel" class="filter-panel hidden bg-white rounded-lg shadow mb-6">
<div class="flex justify-between items-center mb-4">
<h3 class="font-bold text-lg">Filters</h3>
<button id="closeFiltersBtn" class="text-gray-500 hover:text-gray-700">
<i class="fas fa-times"></i>
</button>
</div>
<div id="filterControls" class="space-y-4">
<!-- Filters will be dynamically added here -->
</div>
<div class="flex justify-end gap-2 mt-4">
<button id="applyFiltersBtn" class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded">
Apply Filters
</button>
<button id="resetFiltersBtn" class="bg-gray-200 hover:bg-gray-300 text-gray-800 px-4 py-2 rounded">
Reset
</button>
</div>
</div>
<!-- Column Options Panel -->
<div id="columnOptionsPanel" class="column-options hidden bg-white rounded-lg shadow">
<div class="p-3">
<h3 class="font-bold mb-2">Visible Columns</h3>
<div id="columnCheckboxes" class="space-y-2">
<!-- Column checkboxes will be dynamically added here -->
</div>
<button id="resetColumnsBtn" class="mt-3 text-blue-500 hover:text-blue-700 text-sm">
Reset to Default
</button>
</div>
</div>
<!-- Table Container -->
<div class="table-container bg-white rounded-lg shadow">
<div class="table-wrapper">
<table id="dataTable" class="w-full border-collapse">
<thead class="table-header">
<tr id="tableHeader" class="bg-gray-100 text-left">
<!-- Table headers will be dynamically added here -->
</tr>
</thead>
<tbody id="tableBody">
<!-- Table data will be dynamically added here -->
</tbody>
</table>
</div>
<!-- Pagination -->
<div id="pagination" class="flex items-center justify-between p-4 border-t border-gray-200">
<div class="flex items-center gap-2">
<span class="text-sm text-gray-600">Rows per page:</span>
<select id="rowsPerPage" class="border border-gray-300 rounded px-2 py-1 text-sm">
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</div>
<div class="flex items-center gap-2">
<button id="firstPage" class="px-3 py-1 border rounded disabled:opacity-50" disabled>
<i class="fas fa-angle-double-left"></i>
</button>
<button id="prevPage" class="px-3 py-1 border rounded disabled:opacity-50" disabled>
<i class="fas fa-angle-left"></i>
</button>
<span id="pageInfo" class="text-sm text-gray-600">Page 1 of 1</span>
<button id="nextPage" class="px-3 py-1 border rounded disabled:opacity-50" disabled>
<i class="fas fa-angle-right"></i>
</button>
<button id="lastPage" class="px-3 py-1 border rounded disabled:opacity-50" disabled>
<i class="fas fa-angle-double-right"></i>
</button>
</div>
</div>
</div>
</div>
<!-- Tooltip -->
<div id="tooltip" class="tooltip"></div>
<!-- Modal for full text view -->
<div id="textModal" class="modal">
<div class="modal-content">
<div class="flex justify-between items-center mb-4">
<h3 class="font-bold text-lg" id="modalTitle">Full Text</h3>
<button id="closeModalBtn" class="text-gray-500 hover:text-gray-700">
<i class="fas fa-times"></i>
</button>
</div>
<div id="modalContent" class="max-w-4xl max-h-[70vh] overflow-auto"></div>
</div>
</div>
<script>
// Sample data
const sampleData = [
{ id: 1, name: "John Doe", email: "john.doe@example.com", age: 32, status: "Active", joinDate: "2022-01-15", salary: 75000, department: "Engineering", description: "Senior software engineer with 8 years of experience in web development." },
{ id: 2, name: "Jane Smith", email: "jane.smith@example.com", age: 28, status: "Active", joinDate: "2022-03-22", salary: 68000, department: "Marketing", description: "Digital marketing specialist focused on SEO and content strategy." },
{ id: 3, name: "Robert Johnson", email: "robert.j@example.com", age: 45, status: "Inactive", joinDate: "2021-11-05", salary: 92000, department: "Management", description: "Project manager with extensive experience in agile methodologies." },
{ id: 4, name: "Emily Davis", email: "emily.davis@example.com", age: 31, status: "Active", joinDate: "2022-05-18", salary: 71000, department: "Engineering", description: "Frontend developer specializing in React and Vue.js frameworks." },
{ id: 5, name: "Michael Brown", email: "michael.b@example.com", age: 29, status: "Pending", joinDate: "2022-07-30", salary: 65000, department: "Sales", description: "Sales representative with strong customer relationship skills." },
{ id: 6, name: "Sarah Wilson", email: "sarah.w@example.com", age: 36, status: "Active", joinDate: "2021-09-12", salary: 85000, department: "HR", description: "HR manager responsible for recruitment and employee relations." },
{ id: 7, name: "David Taylor", email: "david.t@example.com", age: 42, status: "Inactive", joinDate: "2020-12-01", salary: 88000, department: "Finance", description: "Financial analyst with expertise in budgeting and forecasting." },
{ id: 8, name: "Jessica Martinez", email: "jessica.m@example.com", age: 27, status: "Active", joinDate: "2022-04-05", salary: 69000, department: "Engineering", description: "Backend developer working primarily with Node.js and Python." },
{ id: 9, name: "Thomas Anderson", email: "thomas.a@example.com", age: 38, status: "Active", joinDate: "2021-06-20", salary: 78000, department: "Product", description: "Product owner with background in UX design and business analysis." },
{ id: 10, name: "Lisa Jackson", email: "lisa.j@example.com", age: 33, status: "Pending", joinDate: "2022-08-15", salary: 72000, department: "Marketing", description: "Social media manager creating engaging content for various platforms." },
{ id: 11, name: "James White", email: "james.w@example.com", age: 40, status: "Active", joinDate: "2020-10-10", salary: 95000, department: "Management", description: "Director of operations overseeing multiple departments and projects." },
{ id: 12, name: "Amanda Harris", email: "amanda.h@example.com", age: 26, status: "Active", joinDate: "2022-02-28", salary: 63000, department: "Sales", description: "Junior sales associate learning the ropes of the business." },
{ id: 13, name: "Daniel Martin", email: "daniel.m@example.com", age: 35, status: "Inactive", joinDate: "2021-04-17", salary: 82000, department: "Engineering", description: "DevOps engineer managing cloud infrastructure and CI/CD pipelines." },
{ id: 14, name: "Jennifer Lee", email: "jennifer.l@example.com", age: 30, status: "Active", joinDate: "2022-06-08", salary: 74000, department: "Product", description: "UX designer creating intuitive interfaces for web and mobile applications." },
{ id: 15, name: "Christopher Walker", email: "chris.w@example.com", age: 44, status: "Active", joinDate: "2020-08-25", salary: 91000, department: "Finance", description: "Senior accountant handling corporate financial statements and audits." }
];
// DOM elements
const tableHeader = document.getElementById('tableHeader');
const tableBody = document.getElementById('tableBody');
const pagination = document.getElementById('pagination');
const pageInfo = document.getElementById('pageInfo');
const firstPageBtn = document.getElementById('firstPage');
const prevPageBtn = document.getElementById('prevPage');
const nextPageBtn = document.getElementById('nextPage');
const lastPageBtn = document.getElementById('lastPage');
const rowsPerPageSelect = document.getElementById('rowsPerPage');
const globalSearchInput = document.getElementById('globalSearch');
const uploadBtn = document.getElementById('uploadBtn');
const downloadBtn = document.getElementById('downloadBtn');
const pasteBtn = document.getElementById('pasteBtn');
const loadConfigBtn = document.getElementById('loadConfigBtn');
const saveConfigBtn = document.getElementById('saveConfigBtn');
const toggleFiltersBtn = document.getElementById('toggleFiltersBtn');
const toggleColumnsBtn = document.getElementById('toggleColumnsBtn');
const filterPanel = document.getElementById('filterPanel');
const columnOptionsPanel = document.getElementById('columnOptionsPanel');
const closeFiltersBtn = document.getElementById('closeFiltersBtn');
const applyFiltersBtn = document.getElementById('applyFiltersBtn');
const resetFiltersBtn = document.getElementById('resetFiltersBtn');
const resetColumnsBtn = document.getElementById('resetColumnsBtn');
const columnCheckboxes = document.getElementById('columnCheckboxes');
const filterControls = document.getElementById('filterControls');
const fileDropArea = document.getElementById('fileDropArea');
const fileInput = document.getElementById('fileInput');
const jsonUrl = document.getElementById('jsonUrl');
const loadUrlBtn = document.getElementById('loadUrlBtn');
const tooltip = document.getElementById('tooltip');
const textModal = document.getElementById('textModal');
const modalContent = document.getElementById('modalContent');
const modalTitle = document.getElementById('modalTitle');
const closeModalBtn = document.getElementById('closeModalBtn');
// State variables
let data = [...sampleData];
let filteredData = [...data];
let currentPage = 1;
let rowsPerPage = parseInt(rowsPerPageSelect.value);
let sortColumn = null;
let sortDirection = 'asc';
let columnConfig = {};
let activeFilters = {};
let isDragging = false;
let dragStartX = 0;
let dragStartWidth = 0;
let dragColumnIndex = -1;
let dragColumnElement = null;
let isDraggingColumn = false;
let dragStartColumnX = 0;
let draggedColumnIndex = -1;
let columnOrder = [];
// Initialize the table
function initTable() {
if (data.length === 0) return;
// Initialize column configuration if not already set
if (Object.keys(columnConfig).length === 0) {
const firstItem = data[0];
columnOrder = Object.keys(firstItem);
Object.keys(firstItem).forEach(key => {
columnConfig[key] = {
visible: true,
width: 200, // Default width
type: detectType(firstItem[key])
};
});
}
renderTableHeaders();
renderTableBody();
renderPagination();
renderColumnOptions();
renderFilterControls();
}
// Detect data type for a value
function detectType(value) {
if (typeof value === 'number') return 'number';
if (typeof value === 'boolean') return 'boolean';
if (Date.parse(value)) return 'date';
if (typeof value === 'string') {
// Check if it's an enum (limited distinct values)
const distinctValues = [...new Set(data.map(item => item[Object.keys(item).find(k => k === Object.keys(item)[0])]))];
if (distinctValues.length <= data.length * 0.2) return 'enum';
return 'string';
}
return 'string';
}
// Render table headers
function renderTableHeaders() {
tableHeader.innerHTML = '';
columnOrder.forEach((key, index) => {
if (!columnConfig[key] || !columnConfig[key].visible) return;
const th = document.createElement('th');
th.className = 'p-3 border-b border-gray-200 font-semibold text-gray-700 relative';
th.style.width = `${columnConfig[key].width}px`;
th.dataset.column = key;
// Column header content
const headerContent = document.createElement('div');
headerContent.className = 'flex items-center justify-between';
const titleSpan = document.createElement('span');
titleSpan.className = 'draggable-header';
titleSpan.textContent = key;
titleSpan.dataset.column = key;
// Sort indicator
const sortIcon = document.createElement('i');
sortIcon.className = 'fas fa-sort sort-icon ml-2 text-gray-400';
if (sortColumn === key) {
sortIcon.classList.add(sortDirection === 'asc' ? 'fa-sort-up' : 'fa-sort-down');
sortIcon.classList.add('text-blue-500');
}
// Column menu button
const menuBtn = document.createElement('button');
menuBtn.className = 'ml-2 text-gray-400 hover:text-gray-600';
menuBtn.innerHTML = '<i class="fas fa-ellipsis-v"></i>';
menuBtn.onclick = (e) => {
e.stopPropagation();
showColumnMenu(e.target.closest('th'), key);
};
headerContent.appendChild(titleSpan);
headerContent.appendChild(sortIcon);
headerContent.appendChild(menuBtn);
th.appendChild(headerContent);
// Resize handle
const resizeHandle = document.createElement('div');
resizeHandle.className = 'resize-handle';
th.appendChild(resizeHandle);
// Add event listeners
th.addEventListener('click', () => sortTable(key));
// Drag and drop for column reordering
th.addEventListener('mousedown', (e) => {
if (e.target.classList.contains('resize-handle')) {
// Column resize
isDragging = true;
dragStartX = e.clientX;
dragStartWidth = th.offsetWidth;
dragColumnIndex = index;
dragColumnElement = th;
document.body.style.cursor = 'col-resize';
} else if (e.target.classList.contains('draggable-header') || e.target.closest('.draggable-header')) {
// Column reorder
isDraggingColumn = true;
draggedColumnIndex = index;
dragStartColumnX = e.clientX;
th.style.opacity = '0.7';
document.body.style.cursor = 'move';
}
});
tableHeader.appendChild(th);
});
}
// Show column menu
function showColumnMenu(headerElement, columnKey) {
// Close any existing menus
document.querySelectorAll('.column-menu').forEach(el => el.remove());
const menu = document.createElement('div');
menu.className = 'column-menu absolute bg-white shadow-lg rounded-md py-1 z-20';
menu.style.top = `${headerElement.offsetTop + headerElement.offsetHeight}px`;
menu.style.left = `${headerElement.offsetLeft}px`;
const menuItems = [
{ label: 'Hide Column', icon: 'fa-eye-slash', action: () => toggleColumnVisibility(columnKey, false) },
{ label: 'Auto Fit Width', icon: 'fa-arrows-alt-h', action: () => autoFitColumn(columnKey) },
{ label: 'Reset Width', icon: 'fa-undo', action: () => resetColumnWidth(columnKey) },
{ label: 'Filter', icon: 'fa-filter', action: () => showFilterForColumn(columnKey) }
];
menuItems.forEach(item => {
const menuItem = document.createElement('button');
menuItem.className = 'w-full text-left px-4 py-2 hover:bg-gray-100 flex items-center gap-2';
menuItem.innerHTML = `<i class="fas ${item.icon}"></i> ${item.label}`;
menuItem.onclick = (e) => {
e.stopPropagation();
item.action();
menu.remove();
};
menu.appendChild(menuItem);
});
headerElement.appendChild(menu);
// Close menu when clicking elsewhere
setTimeout(() => {
const closeMenu = (e) => {
if (!headerElement.contains(e.target)) {
menu.remove();
document.removeEventListener('click', closeMenu);
}
};
document.addEventListener('click', closeMenu);
}, 0);
}
// Toggle column visibility
function toggleColumnVisibility(columnKey, visible) {
if (typeof visible === 'undefined') {
columnConfig[columnKey].visible = !columnConfig[columnKey].visible;
} else {
columnConfig[columnKey].visible = visible;
}
renderTableHeaders();
renderTableBody();
renderColumnOptions();
}
// Auto fit column width
function autoFitColumn(columnKey) {
// Simple implementation - could be enhanced
const maxContentWidth = Math.max(
...data.map(item => {
const value = item[columnKey];
return measureTextWidth(value !== undefined && value !== null ? value.toString() : '', '14px Arial');
}),
measureTextWidth(columnKey, '14px Arial') // Include header width
);
columnConfig[columnKey].width = Math.min(Math.max(maxContentWidth + 40, 100), 500); // Min 100, max 500
renderTableHeaders();
renderTableBody();
}
// Reset column width
function resetColumnWidth(columnKey) {
columnConfig[columnKey].width = 200;
renderTableHeaders();
renderTableBody();
}
// Show filter for specific column
function showFilterForColumn(columnKey) {
filterPanel.classList.remove('hidden');
toggleFiltersBtn.classList.add('bg-blue-500', 'text-white');
toggleFiltersBtn.classList.remove('bg-gray-200', 'text-gray-800');
// Scroll to the filter if it exists
const existingFilter = document.querySelector(`.filter-control[data-column="${columnKey}"]`);
if (existingFilter) {
existingFilter.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
return;
}
// Otherwise add the filter
addFilterControl(columnKey);
}
// Measure text width
function measureTextWidth(text, font) {
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
context.font = font || '14px Arial';
return context.measureText(text).width;
}
// Render table body
function renderTableBody() {
tableBody.innerHTML = '';
if (filteredData.length === 0) {
const tr = document.createElement('tr');
const td = document.createElement('td');
td.className = 'p-4 text-center text-gray-500';
td.colSpan = columnOrder.filter(key => columnConfig[key]?.visible).length;
td.textContent = 'No data available';
tr.appendChild(td);
tableBody.appendChild(tr);
return;
}
const startIndex = (currentPage - 1) * rowsPerPage;
const endIndex = Math.min(startIndex + rowsPerPage, filteredData.length);
for (let i = startIndex; i < endIndex; i++) {
const item = filteredData[i];
const tr = document.createElement('tr');
tr.className = i % 2 === 0 ? 'bg-white' : 'bg-gray-50';
columnOrder.forEach(key => {
if (!columnConfig[key] || !columnConfig[key].visible) return;
const td = document.createElement('td');
td.className = 'p-3 border-b border-gray-200 text-gray-700 relative';
// Handle long text with ellipsis
const cellValue = item[key] !== undefined && item[key] !== null ? item[key].toString() : '';
const cellDiv = document.createElement('div');
cellDiv.className = 'ellipsis-text';
cellDiv.textContent = cellValue;
cellDiv.title = cellValue;
// Add click to view full text for long content
if (cellValue.length > 50) {
cellDiv.style.cursor = 'pointer';
cellDiv.onclick = () => showFullText(key, cellValue);
}
td.appendChild(cellDiv);
tr.appendChild(td);
});
tableBody.appendChild(tr);
}
}
// Show full text in modal
function showFullText(title, content) {
modalTitle.textContent = title;
modalContent.textContent = content;
textModal.style.display = 'flex';
}
// Render pagination
function renderPagination() {
const totalPages = Math.ceil(filteredData.length / rowsPerPage);
pageInfo.textContent = `Page ${currentPage} of ${totalPages}`;
firstPageBtn.disabled = currentPage === 1;
prevPageBtn.disabled = currentPage === 1;
nextPageBtn.disabled = currentPage === totalPages || totalPages === 0;
lastPageBtn.disabled = currentPage === totalPages || totalPages === 0;
}
// Sort table
function sortTable(columnKey) {
if (sortColumn === columnKey) {
sortDirection = sortDirection === 'asc' ? 'desc' : 'asc';
} else {
sortColumn = columnKey;
sortDirection = 'asc';
}
filteredData.sort((a, b) => {
const valA = a[columnKey];
const valB = b[columnKey];
if (valA === valB) return 0;
if (valA === undefined || valA === null) return 1;
if (valB === undefined || valB === null) return -1;
if (typeof valA === 'number' && typeof valB === 'number') {
return sortDirection === 'asc' ? valA - valB : valB - valA;
}
if (typeof valA === 'string' && typeof valB === 'string') {
const strA = valA.toString().toLowerCase();
const strB = valB.toString().toLowerCase();
return sortDirection === 'asc'
? strA.localeCompare(strB)
: strB.localeCompare(strA);
}
if (Date.parse(valA) && Date.parse(valB)) {
const dateA = new Date(valA);
const dateB = new Date(valB);
return sortDirection === 'asc'
? dateA - dateB
: dateB - dateA;
}
return 0;
});
renderTableHeaders();
renderTableBody();
}
// Filter table
function filterTable() {
filteredData = data.filter(item => {
return Object.entries(activeFilters).every(([columnKey, filter]) => {
if (!filter || !filter.value) return true;
const itemValue = item[columnKey];
if (itemValue === undefined || itemValue === null) return false;
const strValue = itemValue.toString().toLowerCase();
const filterValue = filter.value.toString().toLowerCase();
switch (filter.type) {
case 'string':
return strValue.includes(filterValue);
case 'number':
if (isNaN(itemValue) || isNaN(filter.value)) return false;
return filter.operator === '='
? itemValue == filter.value
: filter.operator === '<'
? itemValue < filter.value
: itemValue > filter.value;
case 'date':
const itemDate = new Date(itemValue);
const filterDate = new Date(filter.value);
return filter.operator === '='
? itemDate.getTime() === filterDate.getTime()
: filter.operator === '<'
? itemDate < filterDate
: itemDate > filterDate;
case 'enum':
return strValue === filterValue;
default:
return strValue.includes(filterValue);
}
});
});
// Apply global search if present
if (globalSearchInput.value.trim()) {
const searchTerm = globalSearchInput.value.trim().toLowerCase();
filteredData = filteredData.filter(item => {
return Object.entries(item).some(([key, value]) => {
if (!columnConfig[key]?.visible) return false;
return value !== undefined && value !== null &&
value.toString().toLowerCase().includes(searchTerm);
});
});
}
// Reset to first page after filtering
currentPage = 1;
renderTableBody();
renderPagination();
}
// Render column options
function renderColumnOptions() {
columnCheckboxes.innerHTML = '';
columnOrder.forEach(key => {
const div = document.createElement('div');
div.className = 'flex items-center';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.id = `col-${key}`;
checkbox.className = 'mr-2';
checkbox.checked = columnConfig[key]?.visible || false;
checkbox.onchange = () => toggleColumnVisibility(key, checkbox.checked);
const label = document.createElement('label');
label.htmlFor = `col-${key}`;
label.textContent = key;
label.className = 'text-sm';
div.appendChild(checkbox);
div.appendChild(label);
columnCheckboxes.appendChild(div);
});
}
// Render filter controls
function renderFilterControls() {
filterControls.innerHTML = '';
columnOrder.forEach(key => {
if (!activeFilters[key]) return;
const filter = activeFilters[key];
const div = document.createElement('div');
div.className = 'filter-control border-b border-gray-200 pb-4 mb-4';
div.dataset.column = key;
const header = document.createElement('div');
header.className = 'flex justify-between items-center mb-2';
const title = document.createElement('h4');
title.className = 'font-medium';
title.textContent = key;
const removeBtn = document.createElement('button');
removeBtn.className = 'text-red-500 hover:text-red-700';
removeBtn.innerHTML = '<i class="fas fa-times"></i>';
removeBtn.onclick = () => {
delete activeFilters[key];
renderFilterControls();
filterTable();
};
header.appendChild(title);
header.appendChild(removeBtn);
div.appendChild(header);
// Filter controls based on type
if (filter.type === 'number' || filter.type === 'date') {
const operatorSelect = document.createElement('select');
operatorSelect.className = 'border border-gray-300 rounded px-2 py-1 mr-2';
operatorSelect.value = filter.operator || '=';
operatorSelect.onchange = (e) => {
activeFilters[key].operator = e.target.value;
};
['=', '<', '>'].forEach(op => {
const option = document.createElement('option');
option.value = op;
option.textContent = op === '=' ? 'equals' : op === '<' ? 'less than' : 'greater than';
operatorSelect.appendChild(option);
});
const valueInput = document.createElement('input');
valueInput.type = filter.type === 'date' ? 'date' : 'number';
valueInput.className = 'border border-gray-300 rounded px-2 py-1';
valueInput.value = filter.value || '';
valueInput.onchange = (e) => {
activeFilters[key].value = e.target.value;
};
div.appendChild(operatorSelect);
div.appendChild(valueInput);
}
else if (filter.type === 'enum') {
const distinctValues = [...new Set(data.map(item => item[key]))].sort();
const select = document.createElement('select');
select.className = 'w-full border border-gray-300 rounded px-2 py-1';
select.value = filter.value || '';
const emptyOption = document.createElement('option');
emptyOption.value = '';
emptyOption.textContent = 'Select a value';
select.appendChild(emptyOption);
distinctValues.forEach(value => {
const option = document.createElement('option');
option.value = value;
option.textContent = value;
select.appendChild(option);
});
select.value = filter.value || '';
select.onchange = (e) => {
activeFilters[key].value = e.target.value;
};
div.appendChild(select);
}
else { // string or other types
const input = document.createElement('input');
input.type = 'text';
input.className = 'w-full border border-gray-300 rounded px-2 py-1';
input.placeholder = `Filter by ${key}`;
input.value = filter.value || '';
input.onchange = (e) => {
activeFilters[key].value = e.target.value;
};
div.appendChild(input);
}
filterControls.appendChild(div);
});
}
// Add filter control
function addFilterControl(columnKey) {
if (!columnConfig[columnKey]) return;
// Initialize filter if it doesn't exist
if (!activeFilters[columnKey]) {
activeFilters[columnKey] = {
type: columnConfig[columnKey].type,
value: '',
operator: '='
};
}
renderFilterControls();
// Scroll to the new filter
const newFilter = document.querySelector(`.filter-control[data-column="${columnKey}"]`);
if (newFilter) {
newFilter.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
}
// Event listeners
document.addEventListener('mousemove', (e) => {
// Column resize
if (isDragging && dragColumnElement) {
const width = dragStartWidth + (e.clientX - dragStartX);
const columnKey = dragColumnElement.dataset.column;
columnConfig[columnKey].width = Math.max(width, 50); // Minimum width
dragColumnElement.style.width = `${columnConfig[columnKey].width}px`;
}
// Column reordering
if (isDraggingColumn) {
// Visual feedback could be added here (like a placeholder or shadow)
}
});
document.addEventListener('mouseup', () => {
if (isDragging) {
isDragging = false;
document.body.style.cursor = '';
if (dragColumnElement) {
dragColumnElement.style.opacity = '1';
dragColumnElement = null;
}
}
if (isDraggingColumn) {
isDraggingColumn = false;
document.body.style.cursor = '';
// Update column order if the drag position has changed significantly
if (draggedColumnIndex >= 0) {
const thElements = tableHeader.querySelectorAll('th');
thElements.forEach(th => th.style.opacity = '1');
// Determine the new position based on mouse position
const targetIndex = calculateNewColumnPosition(draggedColumnIndex);
if (targetIndex !== draggedColumnIndex) {
// Update column order
const columnKey = columnOrder[draggedColumnIndex];
columnOrder.splice(draggedColumnIndex, 1);
columnOrder.splice(targetIndex, 0, columnKey);
// Re-render table
renderTableHeaders();
renderTableBody();
}
}
}
});
// Calculate new column position when dragging
function calculateNewColumnPosition(currentIndex) {
const thElements = tableHeader.querySelectorAll('th');
if (thElements.length === 0) return currentIndex;
// Simple implementation - could be enhanced
return currentIndex; // Placeholder
}
// Pagination controls
firstPageBtn.addEventListener('click', () => {
currentPage = 1;
renderTableBody();
renderPagination();
});
prevPageBtn.addEventListener('click', () => {
if (currentPage > 1) {
currentPage--;
renderTableBody();
renderPagination();
}
});
nextPageBtn.addEventListener('click', () => {
const totalPages = Math.ceil(filteredData.length / rowsPerPage);
if (currentPage < totalPages) {
currentPage++;
renderTableBody();
renderPagination();
}
});
lastPageBtn.addEventListener('click', () => {
const totalPages = Math.ceil(filteredData.length / rowsPerPage);
currentPage = totalPages;
renderTableBody();
renderPagination();
});
rowsPerPageSelect.addEventListener('change', () => {
rowsPerPage = parseInt(rowsPerPageSelect.value);
currentPage = 1;
renderTableBody();
renderPagination();
});
// Global search
globalSearchInput.addEventListener('input', () => {
currentPage = 1;
filterTable();
});
// Toggle filters panel
toggleFiltersBtn.addEventListener('click', () => {
filterPanel.classList.toggle('hidden');
if (filterPanel.classList.contains('hidden')) {
toggleFiltersBtn.classList.remove('bg-blue-500', 'text-white');
toggleFiltersBtn.classList.add('bg-gray-200', 'text-gray-800');
} else {
toggleFiltersBtn.classList.add('bg-blue-500', 'text-white');
toggleFiltersBtn.classList.remove('bg-gray-200', 'text-gray-800');
columnOptionsPanel.classList.add('hidden');
}
});
// Toggle columns panel
toggleColumnsBtn.addEventListener('click', (e) => {
columnOptionsPanel.classList.toggle('hidden');
if (columnOptionsPanel.classList.contains('hidden')) {
toggleColumnsBtn.classList.remove('bg-blue-500', 'text-white');
toggleColumnsBtn.classList.add('bg-gray-200', 'text-gray-800');
} else {
toggleColumnsBtn.classList.add('bg-blue-500', 'text-white');
toggleColumnsBtn.classList.remove('bg-gray-200', 'text-gray-800');
filterPanel.classList.add('hidden');
// Position the panel near the button
columnOptionsPanel.style.right = '0';
columnOptionsPanel.style.left = 'auto';
}
});
// Close filters
closeFiltersBtn.addEventListener('click', () => {
filterPanel.classList.add('hidden');
toggleFiltersBtn.classList.remove('bg-blue-500', 'text-white');
toggleFiltersBtn.classList.add('bg-gray-200', 'text-gray-800');
});
// Apply filters
applyFiltersBtn.addEventListener('click', () => {
filterTable();
});
// Reset filters
resetFiltersBtn.addEventListener('click', () => {
activeFilters = {};
globalSearchInput.value = '';
renderFilterControls();
filterTable();
});
// Reset columns
resetColumnsBtn.addEventListener('click', () => {
const firstItem = data[0];
columnOrder = Object.keys(firstItem);
Object.keys(firstItem).forEach(key => {
columnConfig[key] = {
visible: true,
width: 200,
type: detectType(firstItem[key])
};
});
renderTableHeaders();
renderTableBody();
renderColumnOptions();
renderFilterControls();
});
// File upload
uploadBtn.addEventListener('click', () => {
fileInput.click();
});
fileInput.addEventListener('change', (e) => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (event) => {
try {
data = JSON.parse(event.target.result);
filteredData = [...data];
columnConfig = {};
columnOrder = [];
activeFilters = {};
currentPage = 1;
initTable();
} catch (error) {
alert('Error parsing JSON file: ' + error.message);
}
};
reader.readAsText(file);
fileInput.value = ''; // Reset input
});
// File drop area
fileDropArea.addEventListener('dragover', (e) => {
e.preventDefault();
fileDropArea.classList.add('active');
});
fileDropArea.addEventListener('dragleave', () => {
fileDropArea.classList.remove('active');
});
fileDropArea.addEventListener('drop', (e) => {
e.preventDefault();
fileDropArea.classList.remove('active');
const file = e.dataTransfer.files[0];
if (!file) return;
if (file.name.endsWith('.json')) {
fileInput.files = e.dataTransfer.files;
const event = new Event('change');
fileInput.dispatchEvent(event);
} else {
alert('Please upload a JSON file');
}
});
fileDropArea.addEventListener('click', () => {
fileInput.click();
});
// Download data
downloadBtn.addEventListener('click', () => {
const dataStr = 'data:text/json;charset=utf-8,' + encodeURIComponent(JSON.stringify(filteredData, null, 2));
const downloadAnchorNode = document.createElement('a');
downloadAnchorNode.setAttribute('href', dataStr);
downloadAnchorNode.setAttribute('download', 'table_data.json');
document.body.appendChild(downloadAnchorNode);
downloadAnchorNode.click();
downloadAnchorNode.remove();
});
// Paste from clipboard
pasteBtn.addEventListener('click', async () => {
try {
const text = await navigator.clipboard.readText();
data = JSON.parse(text);
filteredData = [...data];
columnConfig = {};
columnOrder = [];
activeFilters = {};
currentPage = 1;
initTable();
} catch (error) {
alert('Error pasting from clipboard: ' + error.message);
}
});
// Load from URL
loadUrlBtn.addEventListener('click', async () => {
const url = jsonUrl.value.trim();
if (!url) return;
try {
const response = await fetch(url);
if (!response.ok) throw new Error('Failed to fetch data');
data = await response.json();
filteredData = [...data];
columnConfig = {};
columnOrder = [];
activeFilters = {};
currentPage = 1;
initTable();
} catch (error) {
alert('Error loading from URL: ' + error.message);
}
});
// Save config
saveConfigBtn.addEventListener('click', () => {
const config = {
columnConfig,
columnOrder,
sortColumn,
sortDirection
};
const dataStr = 'data:text/json;charset=utf-8,' + encodeURIComponent(JSON.stringify(config, null, 2));
const downloadAnchorNode = document.createElement('a');
downloadAnchorNode.setAttribute('href', dataStr);
downloadAnchorNode.setAttribute('download', 'table_config.json');
document.body.appendChild(downloadAnchorNode);
downloadAnchorNode.click();
downloadAnchorNode.remove();
});
// Load config
loadConfigBtn.addEventListener('click', () => {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
input.onchange = e => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = event => {
try {
const config = JSON.parse(event.target.result);
if (config.columnConfig) columnConfig = config.columnConfig;
if (config.columnOrder) columnOrder = config.columnOrder;
if (config.sortColumn) sortColumn = config.sortColumn;
if (config.sortDirection) sortDirection = config.sortDirection;
// Make sure all current columns are included in config
if (data.length > 0) {
const firstItem = data[0];
Object.keys(firstItem).forEach(key => {
if (!columnConfig[key]) {
columnConfig[key] = {
visible: true,
width: 200,
type: detectType(firstItem[key])
};
}
if (!columnOrder.includes(key)) {
columnOrder.push(key);
}
});
}
renderTableHeaders();
renderTableBody();
renderColumnOptions();
renderFilterControls();
renderPagination();
} catch (error) {
alert('Error parsing config file: ' + error.message);
}
};
reader.readAsText(file);
};
input.click();
});
// Close modal
closeModalBtn.addEventListener('click', () => {
textModal.style.display = 'none';
});
// Click outside modal to close
textModal.addEventListener('click', (e) => {
if (e.target === textModal) {
textModal.style.display = 'none';
}
});
// Initialize the table on load
window.addEventListener('load', initTable);
</script>
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - <a href="https://enzostvs-deepsite.hf.space?remix=weisanju/frontend" style="color: #fff;text-decoration: underline;" target="_blank" >🧬 Remix</a></p><p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=Yoleo/tabla-json" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
</html>