Spaces:
Running
Running
<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> |