Skip to content

Instantly share code, notes, and snippets.

@OnurGumus
Last active October 21, 2024 12:59
Show Gist options
  • Save OnurGumus/61507948a0f7c179426194b776127851 to your computer and use it in GitHub Desktop.
Save OnurGumus/61507948a0f7c179426194b776127851 to your computer and use it in GitHub Desktop.
draggable table
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Interactive Draggable Table with Resizable and Reorderable Columns</title>
<style>
/* General table styles */
table.draggable-table {
border-collapse: collapse;
width: 100%;
user-select: none;
/* Prevent text selection during dragging */
table-layout: fixed;
/* Enable fixed table layout for column resizing */
}
table.draggable-table th,
table.draggable-table td {
border: 1px solid #ccc;
padding: 8px;
position: relative;
cursor: pointer;
overflow: hidden;
/* Prevent content overflow when resizing */
word-wrap: break-word;
}
.focused {
outline: 2px solid blue;
}
table.draggable-table th.selected,
table.draggable-table td.selected {
background-color: #b3d4fc;
}
table.draggable-table th.sort-asc::after {
content: ' ▲';
font-size: 12px;
}
table.draggable-table th.sort-desc::after {
content: ' ▼';
font-size: 12px;
}
table.draggable-table th .filter-container {
display: flex;
flex-direction: column;
margin-top: 4px;
}
table.draggable-table th .filter-select {
margin-bottom: 4px;
padding: 2px;
font-size: 12px;
}
table.draggable-table th .filter-input {
width: 100%;
padding: 2px;
font-size: 12px;
box-sizing: border-box;
}
table.draggable-table th.dragging,
table.draggable-table td.dragging {
opacity: 0.5;
}
/* Resizer styles */
.column-resizer {
position: absolute;
right: 0;
top: 0;
bottom: 0;
width: 5px;
cursor: col-resize;
user-select: none;
}
/* Grouping Area Styles */
#grouping-area {
border: 2px dashed #ccc;
padding: 10px;
margin-bottom: 10px;
min-height: 40px;
display: flex;
align-items: center;
gap: 10px;
}
#grouping-area.active {
border-color: #66afe9;
background-color: #e6f7ff;
}
.group-tag {
background-color: #d9edf7;
border: 1px solid #bce8f1;
border-radius: 4px;
padding: 4px 8px;
display: flex;
align-items: center;
gap: 4px;
}
.group-tag .remove-group {
cursor: pointer;
color: #a94442;
font-weight: bold;
}
/* Group Header Styles */
.group-header {
background-color: #f5f5f5;
cursor: pointer;
font-weight: bold;
}
.group-header .toggle-group {
margin-right: 8px;
cursor: pointer;
font-weight: normal;
}
.hidden {
display: none;
}
</style>
</head>
<body>
<!-- Grouping Area -->
<div id="grouping-area">
<span>Drag column headers here to group</span>
</div>
<!-- Draggable Table -->
<table is="draggable-table" class="draggable-table" id="myTable">
<thead>
<tr>
<th draggable="true">Header 1<div class="column-resizer"></div>
</th>
<th draggable="true">Header 2<div class="column-resizer"></div>
</th>
<th draggable="true">Header 3<div class="column-resizer"></div>
</th>
<th draggable="true">Header 4<div class="column-resizer"></div>
</th>
</tr>
<tr class="filter-row">
<th>
<div class="filter-container">
<select class="filter-select">
<option value="contains">Contains</option>
<option value="startswith">Starts With</option>
<option value="endswith">Ends With</option>
<option value="equal">Equal</option>
<option value="greaterthan">Greater Than</option>
<option value="lessthan">Less Than</option>
</select>
<input type="text" class="filter-input" placeholder="Filter Header 1">
</div>
</th>
<th>
<div class="filter-container">
<select class="filter-select">
<option value="contains">Contains</option>
<option value="startswith">Starts With</option>
<option value="endswith">Ends With</option>
<option value="equal">Equal</option>
<option value="greaterthan">Greater Than</option>
<option value="lessthan">Less Than</option>
</select>
<input type="text" class="filter-input" placeholder="Filter Header 2">
</div>
</th>
<th>
<div class="filter-container">
<select class="filter-select">
<option value="contains">Contains</option>
<option value="startswith">Starts With</option>
<option value="endswith">Ends With</option>
<option value="equal">Equal</option>
<option value="greaterthan">Greater Than</option>
<option value="lessthan">Less Than</option>
</select>
<input type="text" class="filter-input" placeholder="Filter Header 3">
</div>
</th>
<th>
<div class="filter-container">
<select class="filter-select">
<option value="contains">Contains</option>
<option value="startswith">Starts With</option>
<option value="endswith">Ends With</option>
<option value="equal">Equal</option>
<option value="greaterthan">Greater Than</option>
<option value="lessthan">Less Than</option>
</select>
<input type="text" class="filter-input" placeholder="Filter Header 4">
</div>
</th>
</tr>
</thead>
<tbody>
<tr>
<td>Apple</td>
<td>Red</td>
<td>10</td>
<td>Yes</td>
</tr>
<tr>
<td>Banana</td>
<td>Yellow</td>
<td>5</td>
<td>No</td>
</tr>
<tr>
<td>Cherry</td>
<td>Red</td>
<td>20</td>
<td>Yes</td>
</tr>
<tr>
<td>Date</td>
<td>Brown</td>
<td>15</td>
<td>No</td>
</tr>
<tr>
<td>Apple</td>
<td>Green</td>
<td>12</td>
<td>Yes</td>
</tr>
<tr>
<td>Banana</td>
<td>Green</td>
<td>7</td>
<td>No</td>
</tr>
<!-- Add more rows as needed -->
</tbody>
</table>
<script>
class DraggableTable extends HTMLTableElement {
constructor() {
super();
// Instantiate managers
this.isResizing = false;
this.isDraggingColumns = false;
this.preventClick = false;
this.sortState = {};
this.filters = [];
this.groupedColumns = []; // Moved groupedColumns here
this.groupData = {}; // Moved groupData here
this.originalRows = [];
// Managers
this.selectionManager = new SelectionManager(this);
this.resizeManager = new ResizeManager(this);
this.dragManager = new DragManager(this);
this.sortingManager = new SortingManager(this);
this.filteringManager = new FilteringManager(this);
this.groupingManager = new GroupingManager(this);
this.navigationManager = new NavigationManager(this);
}
connectedCallback() {
// Initialize filters array based on number of columns
const headerRow = this.querySelector('thead tr:not(.filter-row)');
const columns = headerRow.children.length;
this.filters = Array(columns).fill({
operator: 'contains',
value: ''
});
// Store original rows and initial row order
const tbody = this.querySelector('tbody');
this.originalRows = Array.from(tbody.querySelectorAll('tr'));
// Assign original index to each row
this.originalRows.forEach((row, index) => {
row.originalIndex = index;
});
// Store a copy of the initial row order
this.initialRowsOrder = Array.from(this.originalRows);
// Call connectedCallback of each manager
this.selectionManager.connectedCallback();
this.resizeManager.connectedCallback();
this.dragManager.connectedCallback();
this.sortingManager.connectedCallback();
this.filteringManager.connectedCallback();
this.groupingManager.connectedCallback();
this.navigationManager.connectedCallback();
}
disconnectedCallback() {
// Call disconnectedCallback of each manager
this.selectionManager.disconnectedCallback();
this.resizeManager.disconnectedCallback();
this.dragManager.disconnectedCallback();
this.sortingManager.disconnectedCallback();
this.filteringManager.disconnectedCallback();
this.groupingManager.disconnectedCallback();
this.navigationManager.disconnectedCallback();
}
/* Utility to Get Number of Columns */
columnsCount() {
const headerRow = this.querySelector('thead tr:not(.filter-row)');
return headerRow.children.length;
}
/* Reorder Columns */
reorderColumns(sourceIndex, targetIndex) {
const headerRow = this.querySelector('thead tr:not(.filter-row)');
const filterRow = this.querySelector('thead tr.filter-row');
const tbody = this.querySelector('tbody');
// Reorder headers
const headerCells = Array.from(headerRow.children);
const filterCells = Array.from(filterRow.children);
const sourceHeader = headerCells[sourceIndex];
const sourceFilter = filterCells[sourceIndex];
headerRow.removeChild(sourceHeader);
filterRow.removeChild(sourceFilter);
if (targetIndex < headerCells.length) {
headerRow.insertBefore(sourceHeader, headerRow.children[targetIndex]);
filterRow.insertBefore(sourceFilter, filterRow.children[targetIndex]);
} else {
headerRow.appendChild(sourceHeader);
filterRow.appendChild(sourceFilter);
}
// Reorder cells in each row
const allRows = this.originalRows;
allRows.forEach(row => {
const cells = Array.from(row.children);
const sourceCell = cells[sourceIndex];
row.removeChild(sourceCell);
if (targetIndex < cells.length) {
row.insertBefore(sourceCell, row.children[targetIndex]);
} else {
row.appendChild(sourceCell);
}
});
// Update filters, sortState, and groupedColumns
this.updateColumnIndices();
// Reapply grouping and filtering
this.filteringManager.applyFilters();
}
updateColumnIndices() {
// Update data-col-index attributes
const headerRow = this.querySelector('thead tr:not(.filter-row)');
const filterRow = this.querySelector('thead tr.filter-row');
const headerCells = Array.from(headerRow.children);
const filterCells = Array.from(filterRow.children);
// Build mapping from original index to new index
const oldIndexToNewIndex = {};
headerCells.forEach((th, newIndex) => {
const originalIndex = parseInt(th.getAttribute('data-original-index'), 10);
th.setAttribute('data-col-index', newIndex);
oldIndexToNewIndex[originalIndex] = newIndex;
});
// Update data-col-index for filter cells and inputs
filterCells.forEach((th, index) => {
th.setAttribute('data-col-index', index);
const select = th.querySelector('.filter-select');
const input = th.querySelector('.filter-input');
if (select && input) {
select.setAttribute('data-col-index', index);
input.setAttribute('data-col-index', index);
}
});
// Update filters array based on data-original-index
const newFilters = [];
headerCells.forEach((th, index) => {
const originalIndex = parseInt(th.getAttribute('data-original-index'), 10);
newFilters[index] = this.filters[originalIndex] || {
operator: 'contains',
value: ''
};
});
this.filters = newFilters;
// Update sortState based on data-original-index
const newSortState = {};
for (let key in this.sortState) {
const originalKey = parseInt(key, 10);
const newKey = oldIndexToNewIndex[originalKey];
if (newKey !== undefined) {
newSortState[newKey] = this.sortState[key];
}
}
this.sortState = newSortState;
// Update groupedColumns based on data-original-index
this.groupedColumns = this.groupedColumns.map(originalIndex => oldIndexToNewIndex[originalIndex]);
// Re-attach event listeners for headers
this.sortingManager.attachHeaderEvents();
this.filteringManager.attachFilterEvents();
// Re-render grouping tags
this.groupingManager.renderGroupingTags();
}
}
class SelectionManager {
constructor(table) {
this.table = table;
this.isMouseDown = false;
this.startCell = null;
this.endCell = null;
this.selectedCells = new Set();
// Bind methods
this.handleMouseDown = this.handleMouseDown.bind(this);
this.handleMouseOver = this.handleMouseOver.bind(this);
this.handleMouseUp = this.handleMouseUp.bind(this);
this.handleCopy = this.handleCopy.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
this.handleDocumentMouseDown = this.handleDocumentMouseDown.bind(this);
}
connectedCallback() {
this.table.addEventListener('mousedown', this.handleMouseDown);
this.table.addEventListener('mouseover', this.handleMouseOver);
document.addEventListener('mouseup', this.handleMouseUp);
document.addEventListener('copy', this.handleCopy);
// Add event listeners for Escape key and clicks outside tbody
document.addEventListener('keydown', this.handleKeyDown);
document.addEventListener('mousedown', this.handleDocumentMouseDown);
}
disconnectedCallback() {
this.table.removeEventListener('mousedown', this.handleMouseDown);
this.table.removeEventListener('mouseover', this.handleMouseOver);
document.removeEventListener('mouseup', this.handleMouseUp);
document.removeEventListener('copy', this.handleCopy);
// Remove event listeners
document.removeEventListener('keydown', this.handleKeyDown);
document.removeEventListener('mousedown', this.handleDocumentMouseDown);
}
handleMouseDown(event) {
if (this.table.isResizing) return;
if (event.target.tagName === 'TH' && event.target.draggable) {
this.table.isDraggingColumns = true;
return; // Prevent cell selection
}
if (this.table.isDraggingColumns) return; // Just in case
// Ignore clicks on filter row cells
const cell = event.target.closest('td, th');
if (cell && cell.parentElement.classList.contains('filter-row')) {
return;
}
if (cell && (cell.tagName === 'TD' || cell.tagName === 'TH')) {
this.isMouseDown = true;
this.clearSelection();
this.startCell = cell;
this.endCell = cell;
this.updateSelection();
}
}
handleMouseOver(event) {
if (this.table.isResizing || this.table.isDraggingColumns) return; // Prevent selection during resizing or dragging
const cell = event.target.closest('td, th');
if (this.isMouseDown && cell && !cell.parentElement.classList.contains('filter-row')) {
if (cell.tagName === 'TD' || cell.tagName === 'TH') {
this.endCell = cell;
this.updateSelection();
}
}
}
handleMouseUp(event) {
if (this.isMouseDown) {
this.isMouseDown = false;
}
if (this.table.isDraggingColumns) {
this.table.isDraggingColumns = false;
}
}
handleCopy(event) {
if (this.selectedCells.size === 0) return;
event.preventDefault();
const data = this.getSelectedData();
event.clipboardData.setData('text/plain', data);
}
handleKeyDown(event) {
if (event.key === 'Escape') {
this.clearSelection();
// Clear focus as well
if (this.table.navigationManager) {
this.table.navigationManager.clearFocus();
}
}
}
handleDocumentMouseDown(event) {
const tbody = this.table.querySelector('tbody');
if (!tbody.contains(event.target)) {
this.clearSelection();
// Clear focus as well
if (this.table.navigationManager) {
this.table.navigationManager.clearFocus();
}
}
}
clearSelection() {
this.selectedCells.forEach(cell => cell.classList.remove('selected'));
this.selectedCells.clear();
}
updateSelection() {
if (!this.startCell || !this.endCell) return;
this.clearSelection();
const cells = this.getCellsInRange(this.startCell, this.endCell);
cells.forEach(cell => {
cell.classList.add('selected');
this.selectedCells.add(cell);
});
}
getCellsInRange(start, end) {
const startRow = start.parentElement.rowIndex;
const startCol = start.cellIndex;
const endRow = end.parentElement.rowIndex;
const endCol = end.cellIndex;
const topRow = Math.min(startRow, endRow);
const bottomRow = Math.max(startRow, endRow);
const leftCol = Math.min(startCol, endCol);
const rightCol = Math.max(startCol, endCol);
const cells = [];
const rows = this.table.rows;
for (let r = topRow; r <= bottomRow; r++) {
const row = rows[r];
// Skip filter row
if (row.classList.contains('filter-row')) continue;
for (let c = leftCol; c <= rightCol; c++) {
const cell = row.cells[c];
if (cell) cells.push(cell);
}
}
return cells;
}
getSelectedData() {
if (this.selectedCells.size === 0) return '';
// Organize cells by rows
const rowsMap = new Map();
this.selectedCells.forEach(cell => {
const rowIndex = cell.parentElement.rowIndex;
if (!rowsMap.has(rowIndex)) rowsMap.set(rowIndex, []);
rowsMap.get(rowIndex).push(cell.innerText);
});
// Sort rows
const sortedRows = Array.from(rowsMap.keys()).sort((a, b) => a - b);
// Create tab-separated values
const data = sortedRows.map(rowIndex => {
const cells = rowsMap.get(rowIndex);
return cells.join('\t');
}).join('\n');
return data;
}
}
class ResizeManager {
constructor(table) {
this.table = table;
this.isResizing = false;
this.resizingColumn = null;
this.startX = 0;
this.startWidth = 0;
// Bind methods
this.handleResizeMouseDown = this.handleResizeMouseDown.bind(this);
this.handleResizeMouseMove = this.handleResizeMouseMove.bind(this);
this.handleResizeMouseUp = this.handleResizeMouseUp.bind(this);
}
connectedCallback() {
const headerRow = this.table.querySelector('thead tr:not(.filter-row)');
headerRow.querySelectorAll('th').forEach((th) => {
const resizer = th.querySelector('.column-resizer');
if (resizer) {
resizer.addEventListener('mousedown', (event) => this.handleResizeMouseDown(event, th));
}
});
document.addEventListener('mousemove', this.handleResizeMouseMove);
document.addEventListener('mouseup', this.handleResizeMouseUp);
}
disconnectedCallback() {
const headerRow = this.table.querySelector('thead tr:not(.filter-row)');
headerRow.querySelectorAll('th').forEach((th) => {
const resizer = th.querySelector('.column-resizer');
if (resizer) {
resizer.removeEventListener('mousedown', this.handleResizeMouseDown);
}
});
document.removeEventListener('mousemove', this.handleResizeMouseMove);
document.removeEventListener('mouseup', this.handleResizeMouseUp);
}
handleResizeMouseDown(event, th) {
event.preventDefault();
event.stopPropagation(); // Prevent the event from bubbling up to the header
this.isResizing = true;
this.table.isResizing = true;
this.resizingColumn = th;
this.startX = event.pageX;
this.startWidth = th.offsetWidth;
}
handleResizeMouseMove(event) {
if (!this.isResizing) return;
const deltaX = event.pageX - this.startX;
const newWidth = this.startWidth + deltaX;
if (newWidth > 30) { // Minimum column width
this.resizingColumn.style.width = newWidth + 'px';
}
}
handleResizeMouseUp(event) {
if (this.isResizing) {
this.isResizing = false;
this.table.isResizing = false;
this.resizingColumn = null;
this.table.preventClick = true; // Add this line
}
}
}
class DragManager {
constructor(table) {
this.table = table;
this.isDraggingColumns = false;
// Bind methods
this.handleDragStart = this.handleDragStart.bind(this);
this.handleDragOver = this.handleDragOver.bind(this);
this.handleDrop = this.handleDrop.bind(this);
this.handleDragEnd = this.handleDragEnd.bind(this);
this.handleHeaderDragOver = this.handleHeaderDragOver.bind(this);
this.handleHeaderDrop = this.handleHeaderDrop.bind(this);
this.handleHeaderRowDragOver = this.handleHeaderRowDragOver.bind(this);
this.handleHeaderRowDrop = this.handleHeaderRowDrop.bind(this);
}
connectedCallback() {
const headerRow = this.table.querySelector('thead tr:not(.filter-row)');
headerRow.querySelectorAll('th').forEach((th) => {
th.addEventListener('dragstart', this.handleDragStart);
th.addEventListener('dragover', this.handleHeaderDragOver);
th.addEventListener('drop', this.handleHeaderDrop);
th.addEventListener('dragend', this.handleDragEnd);
});
// Add event listeners to the entire header row to handle drops beyond the last column
headerRow.parentElement.addEventListener('dragover', this.handleHeaderRowDragOver);
headerRow.parentElement.addEventListener('drop', this.handleHeaderRowDrop);
// Setup grouping area
this.setupGroupingArea();
}
disconnectedCallback() {
const headerRow = this.table.querySelector('thead tr:not(.filter-row)');
headerRow.querySelectorAll('th').forEach((th) => {
th.removeEventListener('dragstart', this.handleDragStart);
th.removeEventListener('dragover', this.handleHeaderDragOver);
th.removeEventListener('drop', this.handleHeaderDrop);
th.removeEventListener('dragend', this.handleDragEnd);
});
headerRow.parentElement.removeEventListener('dragover', this.handleHeaderRowDragOver);
headerRow.parentElement.removeEventListener('drop', this.handleHeaderRowDrop);
// Remove grouping area event listeners
const groupingArea = document.getElementById('grouping-area');
groupingArea.removeEventListener('dragover', this.handleDragOver);
groupingArea.removeEventListener('drop', this.handleDrop);
}
setupGroupingArea() {
const groupingArea = document.getElementById('grouping-area');
// Prevent default dragover behavior
groupingArea.addEventListener('dragover', this.handleDragOver);
groupingArea.addEventListener('drop', this.handleDrop);
// Style adjustments
groupingArea.style.display = 'flex';
groupingArea.style.flexWrap = 'wrap';
}
handleDragStart(event) {
const th = event.currentTarget;
const columnIndex = parseInt(th.getAttribute('data-col-index'), 10);
event.dataTransfer.setData('text/plain', columnIndex);
event.dataTransfer.effectAllowed = 'move';
this.isDraggingColumns = true;
this.table.isDraggingColumns = true; // Set dragging flag
}
handleDragEnd(event) {
this.isDraggingColumns = false;
this.table.isDraggingColumns = false; // Reset dragging flag
}
handleDragOver(event) {
event.preventDefault();
event.dataTransfer.dropEffect = 'move';
const groupingArea = document.getElementById('grouping-area');
groupingArea.classList.add('active');
}
handleDrop(event) {
event.preventDefault();
const groupingArea = document.getElementById('grouping-area');
groupingArea.classList.remove('active');
const columnIndex = parseInt(event.dataTransfer.getData('text/plain'), 10);
if (!this.table.groupedColumns.includes(columnIndex)) {
this.table.groupedColumns.push(columnIndex);
this.table.groupingManager.renderGroupingTags();
this.table.groupingManager.applyGrouping();
}
this.isDraggingColumns = false;
this.table.isDraggingColumns = false; // Reset dragging flag
}
handleHeaderDragOver(event) {
event.preventDefault();
event.dataTransfer.dropEffect = 'move';
}
handleHeaderDrop(event) {
event.preventDefault();
const targetTh = event.currentTarget;
const targetIndex = parseInt(targetTh.getAttribute('data-col-index'), 10);
const sourceIndex = parseInt(event.dataTransfer.getData('text/plain'), 10);
if (sourceIndex === targetIndex) return;
this.table.reorderColumns(sourceIndex, targetIndex);
this.isDraggingColumns = false;
this.table.isDraggingColumns = false; // Reset dragging flag after drop
}
// Updated method to handle drag over on the header row
handleHeaderRowDragOver(event) {
event.preventDefault();
event.dataTransfer.dropEffect = 'move';
}
// Updated method to handle drop on the header row (beyond the last column)
handleHeaderRowDrop(event) {
event.preventDefault();
const sourceIndex = parseInt(event.dataTransfer.getData('text/plain'), 10);
// Determine if the drop is beyond the last column
const headerRow = this.table.querySelector('thead tr:not(.filter-row)');
const rect = headerRow.getBoundingClientRect();
const dropX = event.clientX;
// Calculate target index based on drop position
let targetIndex = headerRow.children.length;
for (let i = 0; i < headerRow.children.length; i++) {
const th = headerRow.children[i];
const thRect = th.getBoundingClientRect();
if (dropX < thRect.left + thRect.width / 2) {
targetIndex = i;
break;
}
}
if (sourceIndex === targetIndex || sourceIndex === targetIndex - 1) return;
this.table.reorderColumns(sourceIndex, targetIndex);
this.isDraggingColumns = false;
this.table.isDraggingColumns = false; // Reset dragging flag after drop
}
}
class SortingManager {
constructor(table) {
this.table = table;
// Bind methods
this.handleHeaderClick = this.handleHeaderClick.bind(this);
}
connectedCallback() {
this.attachHeaderEvents();
}
disconnectedCallback() {
const headerRow = this.table.querySelector('thead tr:not(.filter-row)');
headerRow.querySelectorAll('th').forEach((th) => {
th.removeEventListener('click', this.handleHeaderClick);
});
}
attachHeaderEvents() {
const headerRow = this.table.querySelector('thead tr:not(.filter-row)');
headerRow.querySelectorAll('th').forEach((th, index) => {
th.setAttribute('data-col-index', index);
th.setAttribute('data-original-index', index); // Set original index
th.removeEventListener('click', this.handleHeaderClick);
th.addEventListener('click', this.handleHeaderClick);
});
}
handleHeaderClick(event) {
if (
event.target.closest('.filter-container') ||
event.target.closest('.group-tag') ||
event.target.classList.contains('column-resizer') ||
this.table.isResizing ||
this.table.preventClick
) {
this.table.preventClick = false; // Reset preventClick
return;
}
const th = event.currentTarget;
const columnIndex = parseInt(th.getAttribute('data-col-index'), 10);
// Determine the new sort direction
const currentSort = this.table.sortState[columnIndex] || 'none';
let newSort;
if (currentSort === 'none') {
newSort = 'asc';
} else if (currentSort === 'asc') {
newSort = 'desc';
} else {
newSort = 'none';
}
// Update sortState
if (newSort === 'none') {
delete this.table.sortState[columnIndex];
} else {
this.table.sortState = {}; // Reset other sorts
this.table.sortState[columnIndex] = newSort;
}
// Remove existing sort indicators
const headerRow = this.table.querySelector('thead tr:not(.filter-row)');
headerRow.querySelectorAll('th').forEach((th) => {
th.classList.remove('sort-asc', 'sort-desc', 'sorted');
});
// Add sort indicator to the current column
const currentTh = headerRow.children[columnIndex];
if (newSort !== 'none') {
currentTh.classList.add(newSort === 'asc' ? 'sort-asc' : 'sort-desc');
currentTh.classList.add('sorted');
}
// Perform the sorting
this.sortTable(columnIndex, newSort);
}
sortTable(columnIndex, direction) {
let rowsToDisplay;
// Get the rows that are currently visible after filtering
const visibleRows = this.table.originalRows.filter(row => row.style.display !== 'none');
if (direction === 'none') {
// Restore the original row order using initialRowsOrder
rowsToDisplay = this.table.initialRowsOrder.filter(row => visibleRows.includes(row));
} else {
// Sort the visible rows
rowsToDisplay = Array.from(visibleRows).sort((a, b) => {
const cellA = a.cells[columnIndex].innerText.trim().toLowerCase();
const cellB = b.cells[columnIndex].innerText.trim().toLowerCase();
// Attempt numerical comparison
const numA = parseFloat(cellA);
const numB = parseFloat(cellB);
let comparison = 0;
if (!isNaN(numA) && !isNaN(numB)) {
comparison = numA - numB;
} else {
if (cellA < cellB) comparison = -1;
if (cellA > cellB) comparison = 1;
}
return direction === 'asc' ? comparison : -comparison;
});
}
// Re-apply grouping or render sorted rows
if (this.table.groupedColumns.length > 0) {
// Update originalRows with the sorted order
this.table.originalRows = rowsToDisplay;
this.table.groupingManager.applyGrouping();
} else {
const tbody = this.table.querySelector('tbody');
tbody.innerHTML = '';
rowsToDisplay.forEach(row => {
tbody.appendChild(row);
});
}
}
}
class FilteringManager {
constructor(table) {
this.table = table;
// Bind methods
this.handleFilterInput = this.handleFilterInput.bind(this);
}
connectedCallback() {
this.attachFilterEvents();
}
disconnectedCallback() {
const filterRow = this.table.querySelector('thead tr.filter-row');
filterRow.querySelectorAll('.filter-container').forEach((container) => {
const select = container.querySelector('.filter-select');
const input = container.querySelector('.filter-input');
select.removeEventListener('change', this.handleFilterInput);
input.removeEventListener('input', this.handleFilterInput);
});
}
attachFilterEvents() {
const filterRow = this.table.querySelector('thead tr.filter-row');
filterRow.querySelectorAll('.filter-container').forEach((container, index) => {
const select = container.querySelector('.filter-select');
const input = container.querySelector('.filter-input');
select.setAttribute('data-col-index', index);
input.setAttribute('data-col-index', index);
select.removeEventListener('change', this.handleFilterInput);
input.removeEventListener('input', this.handleFilterInput);
select.addEventListener('change', this.handleFilterInput);
input.addEventListener('input', this.handleFilterInput);
});
}
handleFilterInput(event) {
const element = event.target;
const container = element.closest('.filter-container');
const index = parseInt(element.getAttribute('data-col-index'), 10);
const select = container.querySelector('.filter-select');
const input = container.querySelector('.filter-input');
const operator = select.value;
const value = input.value.trim().toLowerCase();
this.table.filters[index] = {
operator,
value
};
this.applyFilters();
}
applyFilters() {
// Apply filters to original rows
this.table.originalRows.forEach(row => {
let visible = true;
this.table.filters.forEach((filter, index) => {
if (filter.value) {
const cellText = row.cells[index].innerText.trim().toLowerCase();
const filterValue = filter.value;
switch (filter.operator) {
case 'contains':
if (!cellText.includes(filterValue)) visible = false;
break;
case 'startswith':
if (!cellText.startsWith(filterValue)) visible = false;
break;
case 'endswith':
if (!cellText.endsWith(filterValue)) visible = false;
break;
case 'equal':
if (cellText !== filterValue) visible = false;
break;
case 'greaterthan':
const cellNumGT = parseFloat(row.cells[index].innerText.trim());
const filterNumGT = parseFloat(filterValue);
if (isNaN(cellNumGT) || isNaN(filterNumGT) || cellNumGT <= filterNumGT) {
visible = false;
}
break;
case 'lessthan':
const cellNumLT = parseFloat(row.cells[index].innerText.trim());
const filterNumLT = parseFloat(filterValue);
if (isNaN(cellNumLT) || isNaN(filterNumLT) || cellNumLT >= filterNumLT) {
visible = false;
}
break;
default:
break;
}
}
});
row.style.display = visible ? '' : 'none';
});
// Re-apply grouping after filtering
if (this.table.groupedColumns.length > 0) {
this.table.groupingManager.applyGrouping();
} else {
// If not grouped, update the table body with filtered rows
const tbody = this.table.querySelector('tbody');
tbody.innerHTML = '';
this.table.originalRows.forEach(row => {
if (row.style.display !== 'none') {
tbody.appendChild(row);
}
});
}
}
}
class NavigationManager {
constructor(table) {
this.table = table;
this.currentCell = null;
// Bind methods
this.handleKeyDown = this.handleKeyDown.bind(this);
this.handleCellClick = this.handleCellClick.bind(this);
this.handleDocumentMouseDown = this.handleDocumentMouseDown.bind(this);
}
connectedCallback() {
// Add event listener for keydown on the table
this.table.addEventListener('keydown', this.handleKeyDown, true); // Use capture phase
// Add event listener for click on the table
this.table.addEventListener('click', this.handleCellClick);
// Add event listener for clicks outside the table
document.addEventListener('mousedown', this.handleDocumentMouseDown);
// Remove tabindex from the table to prevent it from gaining focus
this.table.removeAttribute('tabindex');
}
disconnectedCallback() {
// Remove event listeners
this.table.removeEventListener('keydown', this.handleKeyDown, true);
this.table.removeEventListener('click', this.handleCellClick);
document.removeEventListener('mousedown', this.handleDocumentMouseDown);
}
handleKeyDown(event) {
const key = event.key;
if (key === 'Escape') {
// Clear focus when Escape key is pressed
this.clearFocus();
// Also clear selection if SelectionManager is present
if (this.table.selectionManager) {
this.table.selectionManager.clearSelection();
}
return;
}
if (!['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(key)) {
return;
}
// Prevent default scrolling behavior
event.preventDefault();
if (!this.currentCell) {
return;
}
let {
rowIndex,
cellIndex
} = this.getCellPosition(this.currentCell);
let newRowIndex = rowIndex;
let newCellIndex = cellIndex;
switch (key) {
case 'ArrowUp':
newRowIndex = this.findPreviousRowIndex(newRowIndex);
break;
case 'ArrowDown':
newRowIndex = this.findNextRowIndex(newRowIndex);
break;
case 'ArrowLeft':
newCellIndex = Math.max(cellIndex - 1, 0);
break;
case 'ArrowRight':
newCellIndex = Math.min(cellIndex + 1, this.table.rows[newRowIndex].cells.length - 1);
break;
}
const newRow = this.table.rows[newRowIndex];
if (newRow) {
const newCell = newRow.cells[newCellIndex];
if (newCell) {
this.currentCell = newCell;
this.focusCell(newCell);
}
}
}
handleCellClick(event) {
const cell = event.target.closest('td, th');
// Ignore clicks on filter row cells
if (cell && cell.parentElement.classList.contains('filter-row')) {
return;
}
if (cell && this.table.contains(cell)) {
this.currentCell = cell;
this.focusCell(cell);
}
}
handleDocumentMouseDown(event) {
if (!this.table.contains(event.target)) {
this.clearFocus();
// Also clear selection if SelectionManager is present
if (this.table.selectionManager) {
this.table.selectionManager.clearSelection();
}
}
}
getCellPosition(cell) {
const row = cell.parentElement;
const rowIndex = Array.from(this.table.rows).indexOf(row);
const cellIndex = Array.from(row.cells).indexOf(cell);
return {
rowIndex,
cellIndex
};
}
findPreviousRowIndex(currentIndex) {
let newIndex = currentIndex - 1;
while (newIndex >= 0) {
const row = this.table.rows[newIndex];
if (!row.classList.contains('filter-row')) {
return newIndex;
}
newIndex--;
}
return currentIndex; // Return current index if no previous row found
}
findNextRowIndex(currentIndex) {
let newIndex = currentIndex + 1;
while (newIndex < this.table.rows.length) {
const row = this.table.rows[newIndex];
if (!row.classList.contains('filter-row')) {
return newIndex;
}
newIndex++;
}
return currentIndex; // Return current index if no next row found
}
focusCell(cell) {
// Remove focus class and tabindex from all cells
this.table.querySelectorAll('.focused').forEach(c => {
c.classList.remove('focused');
c.removeAttribute('tabindex');
});
// Add focus class to the current cell
cell.classList.add('focused');
// Set tabindex to make the cell focusable
cell.setAttribute('tabindex', '0');
// Focus the cell
cell.focus();
// Integrate with SelectionManager to select the focused cell
if (this.table.selectionManager) {
this.table.selectionManager.clearSelection();
this.table.selectionManager.startCell = cell;
this.table.selectionManager.endCell = cell;
this.table.selectionManager.updateSelection();
}
// Scroll cell into view if necessary
cell.scrollIntoView({
block: 'nearest',
inline: 'nearest'
});
}
clearFocus() {
if (this.currentCell) {
// Remove focus class and tabindex
this.currentCell.classList.remove('focused');
this.currentCell.removeAttribute('tabindex');
this.currentCell.blur();
this.currentCell = null;
}
}
}
class GroupingManager {
constructor(table) {
this.table = table;
// Remove these lines
// this.groupedColumns = table.groupedColumns;
// this.groupData = table.groupData;
// Bind methods
this.renderGroupingTags = this.renderGroupingTags.bind(this);
this.applyGrouping = this.applyGrouping.bind(this);
}
connectedCallback() {
this.renderGroupingTags();
}
disconnectedCallback() {
// No event listeners to remove
}
renderGroupingTags() {
const groupingArea = document.getElementById('grouping-area');
groupingArea.innerHTML = ''; // Clear existing tags
if (this.table.groupedColumns.length === 0) {
const placeholder = document.createElement('span');
placeholder.textContent = 'Drag column headers here to group';
groupingArea.appendChild(placeholder);
// Reset the table to ungrouped state
this.applyGrouping();
return;
}
this.table.groupedColumns.forEach((colIndex) => {
const headerRow = this.table.querySelector('thead tr:not(.filter-row)');
const th = headerRow.children[colIndex];
const columnName = th.textContent.trim();
const tag = document.createElement('div');
tag.classList.add('group-tag');
tag.setAttribute('data-col-index', colIndex);
const span = document.createElement('span');
span.textContent = columnName;
tag.appendChild(span);
const removeBtn = document.createElement('span');
removeBtn.classList.add('remove-group');
removeBtn.textContent = '×';
removeBtn.addEventListener('click', () => {
this.table.groupedColumns = this.table.groupedColumns.filter(index => index !== colIndex);
this.renderGroupingTags();
this.applyGrouping();
});
tag.appendChild(removeBtn);
groupingArea.appendChild(tag);
});
}
applyGrouping() {
const tbody = this.table.querySelector('tbody');
// If no grouped columns, reset table
if (this.table.groupedColumns.length === 0) {
tbody.innerHTML = '';
// Append only rows that match the filters (visible rows)
this.table.originalRows.forEach(row => {
if (row.style.display !== 'none') {
tbody.appendChild(row);
}
});
return;
}
// Organize rows into groups based on groupedColumns
this.groupData = {}; // Use this.groupData here
this.table.originalRows.forEach(row => {
// Only include visible rows
if (row.style.display === 'none') return;
let groupKey = '';
this.table.groupedColumns.forEach(colIndex => {
groupKey += row.cells[colIndex].innerText.trim() + '||';
});
if (!this.groupData[groupKey]) {
this.groupData[groupKey] = [];
}
this.groupData[groupKey].push(row);
});
// Clear tbody
tbody.innerHTML = '';
// Render grouped rows
Object.keys(this.groupData).forEach(groupKey => {
const groupRows = this.groupData[groupKey];
if (groupRows.length === 0) {
// Skip rendering this group if no rows are visible
return;
}
const groupValues = groupKey.split('||').slice(0, -1); // Remove last empty string
const groupId = groupKey.replace(/\|\|/g, '_');
// Create group header row
const groupHeader = document.createElement('tr');
groupHeader.classList.add('group-header');
groupHeader.setAttribute('data-group-id', groupId);
const toggleCell = document.createElement('td');
toggleCell.colSpan = this.table.columnsCount();
toggleCell.innerHTML = `<span class="toggle-group">▼</span> Group: ${groupValues.join(', ')}`;
groupHeader.appendChild(toggleCell);
// Add click event to toggle group
groupHeader.addEventListener('click', () => {
const toggleSpan = toggleCell.querySelector('.toggle-group');
const isCollapsed = toggleSpan.textContent === '▼';
toggleSpan.textContent = isCollapsed ? '▶' : '▼';
const groupRows = tbody.querySelectorAll(`tr[data-parent-group-id="${groupId}"]`);
groupRows.forEach(gr => {
gr.classList.toggle('hidden', isCollapsed);
});
});
tbody.appendChild(groupHeader);
// Append group rows
groupRows.forEach(row => {
row.style.display = ''; // Ensure row is visible
row.setAttribute('data-parent-group-id', groupId);
tbody.appendChild(row);
});
});
}
}
// Define the custom element
customElements.define('draggable-table', DraggableTable, {
extends: 'table'
});
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Interactive Draggable Table with Resizable and Reorderable Columns</title>
<style>
/* Root variables for easy theming */
:root {
--primary-color: #3498db;
--secondary-color: #2ecc71;
--border-color: #ccc;
--hover-color: rgba(52, 152, 219, 0.1);
--selected-color: rgba(52, 152, 219, 0.2);
--font-color: #333;
--background-color: #fff;
--transition-duration: 0.3s;
}
/* General table styles */
table.draggable-table {
border-collapse: collapse;
width: 100%;
user-select: none;
/* Prevent text selection during dragging */
table-layout: fixed;
/* Enable fixed table layout for column resizing */
font-family: 'Inter', sans-serif;
color: var(--font-color);
background-color: var(--background-color);
border-radius: 8px;
overflow: hidden;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
}
table.draggable-table thead {
background: linear-gradient(to right, var(--primary-color), var(--secondary-color));
color: #fff;
}
table.draggable-table th,
table.draggable-table td {
border: 1px solid var(--border-color);
padding: 12px;
position: relative;
cursor: pointer;
overflow: hidden;
/* Prevent content overflow when resizing */
word-wrap: break-word;
transition: background-color var(--transition-duration), color var(--transition-duration);
}
table.draggable-table th {
position: sticky;
top: 0;
background: inherit;
z-index: 2;
}
/* Hover effects */
table.draggable-table tbody tr:hover {
background-color: var(--hover-color);
}
table.draggable-table th:hover,
table.draggable-table td:hover {
background-color: var(--hover-color);
}
/* Focused cell */
.focused {
outline: 2px solid var(--secondary-color);
outline-offset: -2px;
}
/* Selected cells */
table.draggable-table th.selected,
table.draggable-table td.selected {
background-color: var(--selected-color);
}
/* Sorting indicators */
table.draggable-table th.sorted::after {
content: '';
display: inline-block;
margin-left: 8px;
border: 6px solid transparent;
border-top-color: var(--font-color);
transform: rotate(0deg);
transition: transform var(--transition-duration);
}
table.draggable-table th.sort-asc::after {
transform: rotate(180deg);
}
table.draggable-table th.sort-desc::after {
transform: rotate(0deg);
}
/* Filter input styles */
table.draggable-table th .filter-container {
display: flex;
flex-direction: column;
margin-top: 4px;
}
table.draggable-table th .filter-select {
margin-bottom: 4px;
padding: 4px;
font-size: 12px;
border-radius: 4px;
border: 1px solid var(--border-color);
background-color: #fff;
color: var(--font-color);
}
table.draggable-table th .filter-input {
width: 100%;
padding: 6px;
font-size: 12px;
border-radius: 4px;
border: 1px solid var(--border-color);
background-color: #fff;
color: var(--font-color);
box-sizing: border-box;
}
/* Column resizing */
.column-resizer {
position: absolute;
right: 0;
top: 0;
bottom: 0;
width: 5px;
cursor: col-resize;
user-select: none;
}
/* Draggable column indication */
table.draggable-table th.dragging,
table.draggable-table td.dragging {
opacity: 0.5;
}
/* Smooth transitions */
table.draggable-table th,
table.draggable-table td {
transition: background-color var(--transition-duration), color var(--transition-duration), opacity var(--transition-duration);
}
/* Grouping Area Styles */
#grouping-area {
border: 2px dashed var(--border-color);
padding: 10px;
margin-bottom: 10px;
min-height: 40px;
display: flex;
align-items: center;
gap: 10px;
border-radius: 8px;
background-color: #fafafa;
transition: border-color var(--transition-duration), background-color var(--transition-duration);
}
#grouping-area.active {
border-color: var(--primary-color);
background-color: rgba(52, 152, 219, 0.05);
}
.group-tag {
background-color: var(--secondary-color);
color: #fff;
border-radius: 16px;
padding: 6px 12px;
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
animation: fadeIn var(--transition-duration);
}
.group-tag .remove-group {
cursor: pointer;
color: #fff;
font-weight: bold;
}
.group-tag .remove-group:hover {
color: #e74c3c;
transition: color var(--transition-duration);
}
/* Group Header Styles */
.group-header {
background-color: rgba(52, 152, 219, 0.1);
cursor: pointer;
font-weight: bold;
transition: background-color var(--transition-duration);
}
.group-header:hover {
background-color: rgba(52, 152, 219, 0.15);
}
.group-header .toggle-group {
margin-right: 8px;
cursor: pointer;
font-weight: normal;
transition: transform var(--transition-duration);
}
.group-header.collapsed .toggle-group::before {
content: '►';
display: inline-block;
margin-right: 4px;
}
.group-header.expanded .toggle-group::before {
content: '▼';
display: inline-block;
margin-right: 4px;
}
/* Hidden rows */
.hidden {
display: none;
}
/* Animations */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-5px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Responsive design */
@media (max-width: 768px) {
table.draggable-table th,
table.draggable-table td {
padding: 8px;
}
.group-tag {
font-size: 12px;
padding: 4px 8px;
}
}
/* Scrollbar styling */
table.draggable-table {
scrollbar-width: thin;
scrollbar-color: var(--primary-color) rgba(0, 0, 0, 0.1);
}
table.draggable-table::-webkit-scrollbar {
height: 8px;
width: 8px;
}
table.draggable-table::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.1);
}
table.draggable-table::-webkit-scrollbar-thumb {
background-color: var(--primary-color);
border-radius: 4px;
border: 2px solid rgba(0, 0, 0, 0.1);
}
</style>
</head>
<body>
<!-- Grouping Area -->
<div id="grouping-area">
<span>Drag column headers here to group</span>
</div>
<!-- Draggable Table -->
<table is="draggable-table" class="draggable-table" id="myTable">
<thead>
<tr>
<th draggable="true">Header 1<div class="column-resizer"></div>
</th>
<th draggable="true">Header 2<div class="column-resizer"></div>
</th>
<th draggable="true">Header 3<div class="column-resizer"></div>
</th>
<th draggable="true">Header 4<div class="column-resizer"></div>
</th>
</tr>
<tr class="filter-row">
<th>
<div class="filter-container">
<select class="filter-select">
<option value="contains">Contains</option>
<option value="startswith">Starts With</option>
<option value="endswith">Ends With</option>
<option value="equal">Equal</option>
<option value="greaterthan">Greater Than</option>
<option value="lessthan">Less Than</option>
</select>
<input type="text" class="filter-input" placeholder="Filter Header 1">
</div>
</th>
<th>
<div class="filter-container">
<select class="filter-select">
<option value="contains">Contains</option>
<option value="startswith">Starts With</option>
<option value="endswith">Ends With</option>
<option value="equal">Equal</option>
<option value="greaterthan">Greater Than</option>
<option value="lessthan">Less Than</option>
</select>
<input type="text" class="filter-input" placeholder="Filter Header 2">
</div>
</th>
<th>
<div class="filter-container">
<select class="filter-select">
<option value="contains">Contains</option>
<option value="startswith">Starts With</option>
<option value="endswith">Ends With</option>
<option value="equal">Equal</option>
<option value="greaterthan">Greater Than</option>
<option value="lessthan">Less Than</option>
</select>
<input type="text" class="filter-input" placeholder="Filter Header 3">
</div>
</th>
<th>
<div class="filter-container">
<select class="filter-select">
<option value="contains">Contains</option>
<option value="startswith">Starts With</option>
<option value="endswith">Ends With</option>
<option value="equal">Equal</option>
<option value="greaterthan">Greater Than</option>
<option value="lessthan">Less Than</option>
</select>
<input type="text" class="filter-input" placeholder="Filter Header 4">
</div>
</th>
</tr>
</thead>
<tbody>
<tr>
<td>Apple</td>
<td>Red</td>
<td>10</td>
<td>Yes</td>
</tr>
<tr>
<td>Banana</td>
<td>Yellow</td>
<td>5</td>
<td>No</td>
</tr>
<tr>
<td>Cherry</td>
<td>Red</td>
<td>20</td>
<td>Yes</td>
</tr>
<tr>
<td>Date</td>
<td>Brown</td>
<td>15</td>
<td>No</td>
</tr>
<tr>
<td>Apple</td>
<td>Green</td>
<td>12</td>
<td>Yes</td>
</tr>
<tr>
<td>Banana</td>
<td>Green</td>
<td>7</td>
<td>No</td>
</tr>
<!-- Add more rows as needed -->
</tbody>
</table>
<script>
class DraggableTable extends HTMLTableElement {
constructor() {
super();
// Instantiate managers
this.isResizing = false;
this.isDraggingColumns = false;
this.preventClick = false;
this.sortState = {};
this.filters = [];
this.groupedColumns = []; // Moved groupedColumns here
this.groupData = {}; // Moved groupData here
this.originalRows = [];
// Managers
this.selectionManager = new SelectionManager(this);
this.resizeManager = new ResizeManager(this);
this.dragManager = new DragManager(this);
this.sortingManager = new SortingManager(this);
this.filteringManager = new FilteringManager(this);
this.groupingManager = new GroupingManager(this);
this.navigationManager = new NavigationManager(this);
}
connectedCallback() {
// Initialize filters array based on number of columns
const headerRow = this.querySelector('thead tr:not(.filter-row)');
const columns = headerRow.children.length;
this.filters = Array(columns).fill({
operator: 'contains',
value: ''
});
// Store original rows and initial row order
const tbody = this.querySelector('tbody');
this.originalRows = Array.from(tbody.querySelectorAll('tr'));
// Assign original index to each row
this.originalRows.forEach((row, index) => {
row.originalIndex = index;
});
// Store a copy of the initial row order
this.initialRowsOrder = Array.from(this.originalRows);
// Call connectedCallback of each manager
this.selectionManager.connectedCallback();
this.resizeManager.connectedCallback();
this.dragManager.connectedCallback();
this.sortingManager.connectedCallback();
this.filteringManager.connectedCallback();
this.groupingManager.connectedCallback();
this.navigationManager.connectedCallback();
}
disconnectedCallback() {
// Call disconnectedCallback of each manager
this.selectionManager.disconnectedCallback();
this.resizeManager.disconnectedCallback();
this.dragManager.disconnectedCallback();
this.sortingManager.disconnectedCallback();
this.filteringManager.disconnectedCallback();
this.groupingManager.disconnectedCallback();
this.navigationManager.disconnectedCallback();
}
/* Utility to Get Number of Columns */
columnsCount() {
const headerRow = this.querySelector('thead tr:not(.filter-row)');
return headerRow.children.length;
}
/* Reorder Columns */
reorderColumns(sourceIndex, targetIndex) {
const headerRow = this.querySelector('thead tr:not(.filter-row)');
const filterRow = this.querySelector('thead tr.filter-row');
const tbody = this.querySelector('tbody');
// Reorder headers
const headerCells = Array.from(headerRow.children);
const filterCells = Array.from(filterRow.children);
const sourceHeader = headerCells[sourceIndex];
const sourceFilter = filterCells[sourceIndex];
headerRow.removeChild(sourceHeader);
filterRow.removeChild(sourceFilter);
if (targetIndex < headerCells.length) {
headerRow.insertBefore(sourceHeader, headerRow.children[targetIndex]);
filterRow.insertBefore(sourceFilter, filterRow.children[targetIndex]);
} else {
headerRow.appendChild(sourceHeader);
filterRow.appendChild(sourceFilter);
}
// Reorder cells in each row
const allRows = this.originalRows;
allRows.forEach(row => {
const cells = Array.from(row.children);
const sourceCell = cells[sourceIndex];
row.removeChild(sourceCell);
if (targetIndex < cells.length) {
row.insertBefore(sourceCell, row.children[targetIndex]);
} else {
row.appendChild(sourceCell);
}
});
// Update filters, sortState, and groupedColumns
this.updateColumnIndices();
// Reapply grouping and filtering
this.filteringManager.applyFilters();
}
updateColumnIndices() {
// Update data-col-index attributes
const headerRow = this.querySelector('thead tr:not(.filter-row)');
const filterRow = this.querySelector('thead tr.filter-row');
const headerCells = Array.from(headerRow.children);
const filterCells = Array.from(filterRow.children);
// Build mapping from original index to new index
const oldIndexToNewIndex = {};
headerCells.forEach((th, newIndex) => {
const originalIndex = parseInt(th.getAttribute('data-original-index'), 10);
th.setAttribute('data-col-index', newIndex);
oldIndexToNewIndex[originalIndex] = newIndex;
});
// Update data-col-index for filter cells and inputs
filterCells.forEach((th, index) => {
th.setAttribute('data-col-index', index);
const select = th.querySelector('.filter-select');
const input = th.querySelector('.filter-input');
if (select && input) {
select.setAttribute('data-col-index', index);
input.setAttribute('data-col-index', index);
}
});
// Update filters array based on data-original-index
const newFilters = [];
headerCells.forEach((th, index) => {
const originalIndex = parseInt(th.getAttribute('data-original-index'), 10);
newFilters[index] = this.filters[originalIndex] || {
operator: 'contains',
value: ''
};
});
this.filters = newFilters;
// Update sortState based on data-original-index
const newSortState = {};
for (let key in this.sortState) {
const originalKey = parseInt(key, 10);
const newKey = oldIndexToNewIndex[originalKey];
if (newKey !== undefined) {
newSortState[newKey] = this.sortState[key];
}
}
this.sortState = newSortState;
// Update groupedColumns based on data-original-index
this.groupedColumns = this.groupedColumns.map(originalIndex => oldIndexToNewIndex[originalIndex]);
// Re-attach event listeners for headers
this.sortingManager.attachHeaderEvents();
this.filteringManager.attachFilterEvents();
// Re-render grouping tags
this.groupingManager.renderGroupingTags();
}
}
class SelectionManager {
constructor(table) {
this.table = table;
this.isMouseDown = false;
this.startCell = null;
this.endCell = null;
this.selectedCells = new Set();
// Bind methods
this.handleMouseDown = this.handleMouseDown.bind(this);
this.handleMouseOver = this.handleMouseOver.bind(this);
this.handleMouseUp = this.handleMouseUp.bind(this);
this.handleCopy = this.handleCopy.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
this.handleDocumentMouseDown = this.handleDocumentMouseDown.bind(this);
}
connectedCallback() {
this.table.addEventListener('mousedown', this.handleMouseDown);
this.table.addEventListener('mouseover', this.handleMouseOver);
document.addEventListener('mouseup', this.handleMouseUp);
document.addEventListener('copy', this.handleCopy);
// Add event listeners for Escape key and clicks outside tbody
document.addEventListener('keydown', this.handleKeyDown);
document.addEventListener('mousedown', this.handleDocumentMouseDown);
}
disconnectedCallback() {
this.table.removeEventListener('mousedown', this.handleMouseDown);
this.table.removeEventListener('mouseover', this.handleMouseOver);
document.removeEventListener('mouseup', this.handleMouseUp);
document.removeEventListener('copy', this.handleCopy);
// Remove event listeners
document.removeEventListener('keydown', this.handleKeyDown);
document.removeEventListener('mousedown', this.handleDocumentMouseDown);
}
handleMouseDown(event) {
if (this.table.isResizing) return;
if (event.target.tagName === 'TH' && event.target.draggable) {
this.table.isDraggingColumns = true;
return; // Prevent cell selection
}
if (this.table.isDraggingColumns) return; // Just in case
// Ignore clicks on filter row cells
const cell = event.target.closest('td, th');
if (cell && cell.parentElement.classList.contains('filter-row')) {
return;
}
if (cell && (cell.tagName === 'TD' || cell.tagName === 'TH')) {
this.isMouseDown = true;
this.clearSelection();
this.startCell = cell;
this.endCell = cell;
this.updateSelection();
}
}
handleMouseOver(event) {
if (this.table.isResizing || this.table.isDraggingColumns) return; // Prevent selection during resizing or dragging
const cell = event.target.closest('td, th');
if (this.isMouseDown && cell && !cell.parentElement.classList.contains('filter-row')) {
if (cell.tagName === 'TD' || cell.tagName === 'TH') {
this.endCell = cell;
this.updateSelection();
}
}
}
handleMouseUp(event) {
if (this.isMouseDown) {
this.isMouseDown = false;
}
if (this.table.isDraggingColumns) {
this.table.isDraggingColumns = false;
}
}
handleCopy(event) {
if (this.selectedCells.size === 0) return;
event.preventDefault();
const data = this.getSelectedData();
event.clipboardData.setData('text/plain', data);
}
handleKeyDown(event) {
if (event.key === 'Escape') {
this.clearSelection();
// Clear focus as well
if (this.table.navigationManager) {
this.table.navigationManager.clearFocus();
}
}
}
handleDocumentMouseDown(event) {
const tbody = this.table.querySelector('tbody');
if (!tbody.contains(event.target)) {
this.clearSelection();
// Clear focus as well
if (this.table.navigationManager) {
this.table.navigationManager.clearFocus();
}
}
}
clearSelection() {
this.selectedCells.forEach(cell => cell.classList.remove('selected'));
this.selectedCells.clear();
}
updateSelection() {
if (!this.startCell || !this.endCell) return;
this.clearSelection();
const cells = this.getCellsInRange(this.startCell, this.endCell);
cells.forEach(cell => {
cell.classList.add('selected');
this.selectedCells.add(cell);
});
}
getCellsInRange(start, end) {
const startRow = start.parentElement.rowIndex;
const startCol = start.cellIndex;
const endRow = end.parentElement.rowIndex;
const endCol = end.cellIndex;
const topRow = Math.min(startRow, endRow);
const bottomRow = Math.max(startRow, endRow);
const leftCol = Math.min(startCol, endCol);
const rightCol = Math.max(startCol, endCol);
const cells = [];
const rows = this.table.rows;
for (let r = topRow; r <= bottomRow; r++) {
const row = rows[r];
// Skip filter row
if (row.classList.contains('filter-row')) continue;
for (let c = leftCol; c <= rightCol; c++) {
const cell = row.cells[c];
if (cell) cells.push(cell);
}
}
return cells;
}
getSelectedData() {
if (this.selectedCells.size === 0) return '';
// Organize cells by rows
const rowsMap = new Map();
this.selectedCells.forEach(cell => {
const rowIndex = cell.parentElement.rowIndex;
if (!rowsMap.has(rowIndex)) rowsMap.set(rowIndex, []);
rowsMap.get(rowIndex).push(cell.innerText);
});
// Sort rows
const sortedRows = Array.from(rowsMap.keys()).sort((a, b) => a - b);
// Create tab-separated values
const data = sortedRows.map(rowIndex => {
const cells = rowsMap.get(rowIndex);
return cells.join('\t');
}).join('\n');
return data;
}
}
class ResizeManager {
constructor(table) {
this.table = table;
this.isResizing = false;
this.resizingColumn = null;
this.startX = 0;
this.startWidth = 0;
// Bind methods
this.handleResizeMouseDown = this.handleResizeMouseDown.bind(this);
this.handleResizeMouseMove = this.handleResizeMouseMove.bind(this);
this.handleResizeMouseUp = this.handleResizeMouseUp.bind(this);
}
connectedCallback() {
const headerRow = this.table.querySelector('thead tr:not(.filter-row)');
headerRow.querySelectorAll('th').forEach((th) => {
const resizer = th.querySelector('.column-resizer');
if (resizer) {
resizer.addEventListener('mousedown', (event) => this.handleResizeMouseDown(event, th));
}
});
document.addEventListener('mousemove', this.handleResizeMouseMove);
document.addEventListener('mouseup', this.handleResizeMouseUp);
}
disconnectedCallback() {
const headerRow = this.table.querySelector('thead tr:not(.filter-row)');
headerRow.querySelectorAll('th').forEach((th) => {
const resizer = th.querySelector('.column-resizer');
if (resizer) {
resizer.removeEventListener('mousedown', this.handleResizeMouseDown);
}
});
document.removeEventListener('mousemove', this.handleResizeMouseMove);
document.removeEventListener('mouseup', this.handleResizeMouseUp);
}
handleResizeMouseDown(event, th) {
event.preventDefault();
event.stopPropagation(); // Prevent the event from bubbling up to the header
this.isResizing = true;
this.table.isResizing = true;
this.resizingColumn = th;
this.startX = event.pageX;
this.startWidth = th.offsetWidth;
}
handleResizeMouseMove(event) {
if (!this.isResizing) return;
const deltaX = event.pageX - this.startX;
const newWidth = this.startWidth + deltaX;
if (newWidth > 30) { // Minimum column width
this.resizingColumn.style.width = newWidth + 'px';
}
}
handleResizeMouseUp(event) {
if (this.isResizing) {
this.isResizing = false;
this.table.isResizing = false;
this.resizingColumn = null;
this.table.preventClick = true; // Add this line
}
}
}
class DragManager {
constructor(table) {
this.table = table;
this.isDraggingColumns = false;
// Bind methods
this.handleDragStart = this.handleDragStart.bind(this);
this.handleDragOver = this.handleDragOver.bind(this);
this.handleDrop = this.handleDrop.bind(this);
this.handleDragEnd = this.handleDragEnd.bind(this);
this.handleHeaderDragOver = this.handleHeaderDragOver.bind(this);
this.handleHeaderDrop = this.handleHeaderDrop.bind(this);
this.handleHeaderRowDragOver = this.handleHeaderRowDragOver.bind(this);
this.handleHeaderRowDrop = this.handleHeaderRowDrop.bind(this);
}
connectedCallback() {
const headerRow = this.table.querySelector('thead tr:not(.filter-row)');
headerRow.querySelectorAll('th').forEach((th) => {
th.addEventListener('dragstart', this.handleDragStart);
th.addEventListener('dragover', this.handleHeaderDragOver);
th.addEventListener('drop', this.handleHeaderDrop);
th.addEventListener('dragend', this.handleDragEnd);
});
// Add event listeners to the entire header row to handle drops beyond the last column
headerRow.parentElement.addEventListener('dragover', this.handleHeaderRowDragOver);
headerRow.parentElement.addEventListener('drop', this.handleHeaderRowDrop);
// Setup grouping area
this.setupGroupingArea();
}
disconnectedCallback() {
const headerRow = this.table.querySelector('thead tr:not(.filter-row)');
headerRow.querySelectorAll('th').forEach((th) => {
th.removeEventListener('dragstart', this.handleDragStart);
th.removeEventListener('dragover', this.handleHeaderDragOver);
th.removeEventListener('drop', this.handleHeaderDrop);
th.removeEventListener('dragend', this.handleDragEnd);
});
headerRow.parentElement.removeEventListener('dragover', this.handleHeaderRowDragOver);
headerRow.parentElement.removeEventListener('drop', this.handleHeaderRowDrop);
// Remove grouping area event listeners
const groupingArea = document.getElementById('grouping-area');
groupingArea.removeEventListener('dragover', this.handleDragOver);
groupingArea.removeEventListener('drop', this.handleDrop);
}
setupGroupingArea() {
const groupingArea = document.getElementById('grouping-area');
// Prevent default dragover behavior
groupingArea.addEventListener('dragover', this.handleDragOver);
groupingArea.addEventListener('drop', this.handleDrop);
// Style adjustments
groupingArea.style.display = 'flex';
groupingArea.style.flexWrap = 'wrap';
}
handleDragStart(event) {
const th = event.currentTarget;
const columnIndex = parseInt(th.getAttribute('data-col-index'), 10);
event.dataTransfer.setData('text/plain', columnIndex);
event.dataTransfer.effectAllowed = 'move';
this.isDraggingColumns = true;
this.table.isDraggingColumns = true; // Set dragging flag
}
handleDragEnd(event) {
this.isDraggingColumns = false;
this.table.isDraggingColumns = false; // Reset dragging flag
}
handleDragOver(event) {
event.preventDefault();
event.dataTransfer.dropEffect = 'move';
const groupingArea = document.getElementById('grouping-area');
groupingArea.classList.add('active');
}
handleDrop(event) {
event.preventDefault();
const groupingArea = document.getElementById('grouping-area');
groupingArea.classList.remove('active');
const columnIndex = parseInt(event.dataTransfer.getData('text/plain'), 10);
if (!this.table.groupedColumns.includes(columnIndex)) {
this.table.groupedColumns.push(columnIndex);
this.table.groupingManager.renderGroupingTags();
this.table.groupingManager.applyGrouping();
}
this.isDraggingColumns = false;
this.table.isDraggingColumns = false; // Reset dragging flag
}
handleHeaderDragOver(event) {
event.preventDefault();
event.dataTransfer.dropEffect = 'move';
}
handleHeaderDrop(event) {
event.preventDefault();
const targetTh = event.currentTarget;
const targetIndex = parseInt(targetTh.getAttribute('data-col-index'), 10);
const sourceIndex = parseInt(event.dataTransfer.getData('text/plain'), 10);
if (sourceIndex === targetIndex) return;
this.table.reorderColumns(sourceIndex, targetIndex);
this.isDraggingColumns = false;
this.table.isDraggingColumns = false; // Reset dragging flag after drop
}
// Updated method to handle drag over on the header row
handleHeaderRowDragOver(event) {
event.preventDefault();
event.dataTransfer.dropEffect = 'move';
}
// Updated method to handle drop on the header row (beyond the last column)
handleHeaderRowDrop(event) {
event.preventDefault();
const sourceIndex = parseInt(event.dataTransfer.getData('text/plain'), 10);
// Determine if the drop is beyond the last column
const headerRow = this.table.querySelector('thead tr:not(.filter-row)');
const rect = headerRow.getBoundingClientRect();
const dropX = event.clientX;
// Calculate target index based on drop position
let targetIndex = headerRow.children.length;
for (let i = 0; i < headerRow.children.length; i++) {
const th = headerRow.children[i];
const thRect = th.getBoundingClientRect();
if (dropX < thRect.left + thRect.width / 2) {
targetIndex = i;
break;
}
}
if (sourceIndex === targetIndex || sourceIndex === targetIndex - 1) return;
this.table.reorderColumns(sourceIndex, targetIndex);
this.isDraggingColumns = false;
this.table.isDraggingColumns = false; // Reset dragging flag after drop
}
}
class SortingManager {
constructor(table) {
this.table = table;
// Bind methods
this.handleHeaderClick = this.handleHeaderClick.bind(this);
}
connectedCallback() {
this.attachHeaderEvents();
}
disconnectedCallback() {
const headerRow = this.table.querySelector('thead tr:not(.filter-row)');
headerRow.querySelectorAll('th').forEach((th) => {
th.removeEventListener('click', this.handleHeaderClick);
});
}
attachHeaderEvents() {
const headerRow = this.table.querySelector('thead tr:not(.filter-row)');
headerRow.querySelectorAll('th').forEach((th, index) => {
th.setAttribute('data-col-index', index);
th.setAttribute('data-original-index', index); // Set original index
th.removeEventListener('click', this.handleHeaderClick);
th.addEventListener('click', this.handleHeaderClick);
});
}
handleHeaderClick(event) {
if (
event.target.closest('.filter-container') ||
event.target.closest('.group-tag') ||
event.target.classList.contains('column-resizer') ||
this.table.isResizing ||
this.table.preventClick
) {
this.table.preventClick = false; // Reset preventClick
return;
}
const th = event.currentTarget;
const columnIndex = parseInt(th.getAttribute('data-col-index'), 10);
// Determine the new sort direction
const currentSort = this.table.sortState[columnIndex] || 'none';
let newSort;
if (currentSort === 'none') {
newSort = 'asc';
} else if (currentSort === 'asc') {
newSort = 'desc';
} else {
newSort = 'none';
}
// Update sortState
if (newSort === 'none') {
delete this.table.sortState[columnIndex];
} else {
this.table.sortState = {}; // Reset other sorts
this.table.sortState[columnIndex] = newSort;
}
// Remove existing sort indicators
const headerRow = this.table.querySelector('thead tr:not(.filter-row)');
headerRow.querySelectorAll('th').forEach((th) => {
th.classList.remove('sort-asc', 'sort-desc', 'sorted');
});
// Add sort indicator to the current column
const currentTh = headerRow.children[columnIndex];
if (newSort !== 'none') {
currentTh.classList.add(newSort === 'asc' ? 'sort-asc' : 'sort-desc');
currentTh.classList.add('sorted');
}
// Perform the sorting
this.sortTable(columnIndex, newSort);
}
sortTable(columnIndex, direction) {
let rowsToDisplay;
// Get the rows that are currently visible after filtering
const visibleRows = this.table.originalRows.filter(row => row.style.display !== 'none');
if (direction === 'none') {
// Restore the original row order using initialRowsOrder
rowsToDisplay = this.table.initialRowsOrder.filter(row => visibleRows.includes(row));
} else {
// Sort the visible rows
rowsToDisplay = Array.from(visibleRows).sort((a, b) => {
const cellA = a.cells[columnIndex].innerText.trim().toLowerCase();
const cellB = b.cells[columnIndex].innerText.trim().toLowerCase();
// Attempt numerical comparison
const numA = parseFloat(cellA);
const numB = parseFloat(cellB);
let comparison = 0;
if (!isNaN(numA) && !isNaN(numB)) {
comparison = numA - numB;
} else {
if (cellA < cellB) comparison = -1;
if (cellA > cellB) comparison = 1;
}
return direction === 'asc' ? comparison : -comparison;
});
}
// Re-apply grouping or render sorted rows
if (this.table.groupedColumns.length > 0) {
// Update originalRows with the sorted order
this.table.originalRows = rowsToDisplay;
this.table.groupingManager.applyGrouping();
} else {
const tbody = this.table.querySelector('tbody');
tbody.innerHTML = '';
rowsToDisplay.forEach(row => {
tbody.appendChild(row);
});
}
}
}
class FilteringManager {
constructor(table) {
this.table = table;
// Bind methods
this.handleFilterInput = this.handleFilterInput.bind(this);
}
connectedCallback() {
this.attachFilterEvents();
}
disconnectedCallback() {
const filterRow = this.table.querySelector('thead tr.filter-row');
filterRow.querySelectorAll('.filter-container').forEach((container) => {
const select = container.querySelector('.filter-select');
const input = container.querySelector('.filter-input');
select.removeEventListener('change', this.handleFilterInput);
input.removeEventListener('input', this.handleFilterInput);
});
}
attachFilterEvents() {
const filterRow = this.table.querySelector('thead tr.filter-row');
filterRow.querySelectorAll('.filter-container').forEach((container, index) => {
const select = container.querySelector('.filter-select');
const input = container.querySelector('.filter-input');
select.setAttribute('data-col-index', index);
input.setAttribute('data-col-index', index);
select.removeEventListener('change', this.handleFilterInput);
input.removeEventListener('input', this.handleFilterInput);
select.addEventListener('change', this.handleFilterInput);
input.addEventListener('input', this.handleFilterInput);
});
}
handleFilterInput(event) {
const element = event.target;
const container = element.closest('.filter-container');
const index = parseInt(element.getAttribute('data-col-index'), 10);
const select = container.querySelector('.filter-select');
const input = container.querySelector('.filter-input');
const operator = select.value;
const value = input.value.trim().toLowerCase();
this.table.filters[index] = {
operator,
value
};
this.applyFilters();
}
applyFilters() {
// Apply filters to original rows
this.table.originalRows.forEach(row => {
let visible = true;
this.table.filters.forEach((filter, index) => {
if (filter.value) {
const cellText = row.cells[index].innerText.trim().toLowerCase();
const filterValue = filter.value;
switch (filter.operator) {
case 'contains':
if (!cellText.includes(filterValue)) visible = false;
break;
case 'startswith':
if (!cellText.startsWith(filterValue)) visible = false;
break;
case 'endswith':
if (!cellText.endsWith(filterValue)) visible = false;
break;
case 'equal':
if (cellText !== filterValue) visible = false;
break;
case 'greaterthan':
const cellNumGT = parseFloat(row.cells[index].innerText.trim());
const filterNumGT = parseFloat(filterValue);
if (isNaN(cellNumGT) || isNaN(filterNumGT) || cellNumGT <= filterNumGT) {
visible = false;
}
break;
case 'lessthan':
const cellNumLT = parseFloat(row.cells[index].innerText.trim());
const filterNumLT = parseFloat(filterValue);
if (isNaN(cellNumLT) || isNaN(filterNumLT) || cellNumLT >= filterNumLT) {
visible = false;
}
break;
default:
break;
}
}
});
row.style.display = visible ? '' : 'none';
});
// Re-apply grouping after filtering
if (this.table.groupedColumns.length > 0) {
this.table.groupingManager.applyGrouping();
} else {
// If not grouped, update the table body with filtered rows
const tbody = this.table.querySelector('tbody');
tbody.innerHTML = '';
this.table.originalRows.forEach(row => {
if (row.style.display !== 'none') {
tbody.appendChild(row);
}
});
}
}
}
class NavigationManager {
constructor(table) {
this.table = table;
this.currentCell = null;
// Bind methods
this.handleKeyDown = this.handleKeyDown.bind(this);
this.handleCellClick = this.handleCellClick.bind(this);
this.handleDocumentMouseDown = this.handleDocumentMouseDown.bind(this);
}
connectedCallback() {
// Add event listener for keydown on the table
this.table.addEventListener('keydown', this.handleKeyDown, true); // Use capture phase
// Add event listener for click on the table
this.table.addEventListener('click', this.handleCellClick);
// Add event listener for clicks outside the table
document.addEventListener('mousedown', this.handleDocumentMouseDown);
// Remove tabindex from the table to prevent it from gaining focus
this.table.removeAttribute('tabindex');
}
disconnectedCallback() {
// Remove event listeners
this.table.removeEventListener('keydown', this.handleKeyDown, true);
this.table.removeEventListener('click', this.handleCellClick);
document.removeEventListener('mousedown', this.handleDocumentMouseDown);
}
handleKeyDown(event) {
const key = event.key;
if (key === 'Escape') {
// Clear focus when Escape key is pressed
this.clearFocus();
// Also clear selection if SelectionManager is present
if (this.table.selectionManager) {
this.table.selectionManager.clearSelection();
}
return;
}
if (!['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(key)) {
return;
}
// Prevent default scrolling behavior
event.preventDefault();
if (!this.currentCell) {
return;
}
let {
rowIndex,
cellIndex
} = this.getCellPosition(this.currentCell);
let newRowIndex = rowIndex;
let newCellIndex = cellIndex;
switch (key) {
case 'ArrowUp':
newRowIndex = this.findPreviousRowIndex(newRowIndex);
break;
case 'ArrowDown':
newRowIndex = this.findNextRowIndex(newRowIndex);
break;
case 'ArrowLeft':
newCellIndex = Math.max(cellIndex - 1, 0);
break;
case 'ArrowRight':
newCellIndex = Math.min(cellIndex + 1, this.table.rows[newRowIndex].cells.length - 1);
break;
}
const newRow = this.table.rows[newRowIndex];
if (newRow) {
const newCell = newRow.cells[newCellIndex];
if (newCell) {
this.currentCell = newCell;
this.focusCell(newCell);
}
}
}
handleCellClick(event) {
const cell = event.target.closest('td, th');
// Ignore clicks on filter row cells
if (cell && cell.parentElement.classList.contains('filter-row')) {
return;
}
if (cell && this.table.contains(cell)) {
this.currentCell = cell;
this.focusCell(cell);
}
}
handleDocumentMouseDown(event) {
if (!this.table.contains(event.target)) {
this.clearFocus();
// Also clear selection if SelectionManager is present
if (this.table.selectionManager) {
this.table.selectionManager.clearSelection();
}
}
}
getCellPosition(cell) {
const row = cell.parentElement;
const rowIndex = Array.from(this.table.rows).indexOf(row);
const cellIndex = Array.from(row.cells).indexOf(cell);
return {
rowIndex,
cellIndex
};
}
findPreviousRowIndex(currentIndex) {
let newIndex = currentIndex - 1;
while (newIndex >= 0) {
const row = this.table.rows[newIndex];
if (!row.classList.contains('filter-row')) {
return newIndex;
}
newIndex--;
}
return currentIndex; // Return current index if no previous row found
}
findNextRowIndex(currentIndex) {
let newIndex = currentIndex + 1;
while (newIndex < this.table.rows.length) {
const row = this.table.rows[newIndex];
if (!row.classList.contains('filter-row')) {
return newIndex;
}
newIndex++;
}
return currentIndex; // Return current index if no next row found
}
focusCell(cell) {
// Remove focus class and tabindex from all cells
this.table.querySelectorAll('.focused').forEach(c => {
c.classList.remove('focused');
c.removeAttribute('tabindex');
});
// Add focus class to the current cell
cell.classList.add('focused');
// Set tabindex to make the cell focusable
cell.setAttribute('tabindex', '0');
// Focus the cell
cell.focus();
// Integrate with SelectionManager to select the focused cell
if (this.table.selectionManager) {
this.table.selectionManager.clearSelection();
this.table.selectionManager.startCell = cell;
this.table.selectionManager.endCell = cell;
this.table.selectionManager.updateSelection();
}
// Scroll cell into view if necessary
cell.scrollIntoView({
block: 'nearest',
inline: 'nearest'
});
}
clearFocus() {
if (this.currentCell) {
// Remove focus class and tabindex
this.currentCell.classList.remove('focused');
this.currentCell.removeAttribute('tabindex');
this.currentCell.blur();
this.currentCell = null;
}
}
}
class GroupingManager {
constructor(table) {
this.table = table;
// Remove these lines
// this.groupedColumns = table.groupedColumns;
// this.groupData = table.groupData;
// Bind methods
this.renderGroupingTags = this.renderGroupingTags.bind(this);
this.applyGrouping = this.applyGrouping.bind(this);
}
connectedCallback() {
this.renderGroupingTags();
}
disconnectedCallback() {
// No event listeners to remove
}
renderGroupingTags() {
const groupingArea = document.getElementById('grouping-area');
groupingArea.innerHTML = ''; // Clear existing tags
if (this.table.groupedColumns.length === 0) {
const placeholder = document.createElement('span');
placeholder.textContent = 'Drag column headers here to group';
groupingArea.appendChild(placeholder);
// Reset the table to ungrouped state
this.applyGrouping();
return;
}
this.table.groupedColumns.forEach((colIndex) => {
const headerRow = this.table.querySelector('thead tr:not(.filter-row)');
const th = headerRow.children[colIndex];
const columnName = th.textContent.trim();
const tag = document.createElement('div');
tag.classList.add('group-tag');
tag.setAttribute('data-col-index', colIndex);
const span = document.createElement('span');
span.textContent = columnName;
tag.appendChild(span);
const removeBtn = document.createElement('span');
removeBtn.classList.add('remove-group');
removeBtn.textContent = '×';
removeBtn.addEventListener('click', () => {
this.table.groupedColumns = this.table.groupedColumns.filter(index => index !== colIndex);
this.renderGroupingTags();
this.applyGrouping();
});
tag.appendChild(removeBtn);
groupingArea.appendChild(tag);
});
}
applyGrouping() {
const tbody = this.table.querySelector('tbody');
// If no grouped columns, reset table
if (this.table.groupedColumns.length === 0) {
tbody.innerHTML = '';
// Append only rows that match the filters (visible rows)
this.table.originalRows.forEach(row => {
if (row.style.display !== 'none') {
tbody.appendChild(row);
}
});
return;
}
// Organize rows into groups based on groupedColumns
this.groupData = {}; // Use this.groupData here
this.table.originalRows.forEach(row => {
// Only include visible rows
if (row.style.display === 'none') return;
let groupKey = '';
this.table.groupedColumns.forEach(colIndex => {
groupKey += row.cells[colIndex].innerText.trim() + '||';
});
if (!this.groupData[groupKey]) {
this.groupData[groupKey] = [];
}
this.groupData[groupKey].push(row);
});
// Clear tbody
tbody.innerHTML = '';
// Render grouped rows
Object.keys(this.groupData).forEach(groupKey => {
const groupRows = this.groupData[groupKey];
if (groupRows.length === 0) {
// Skip rendering this group if no rows are visible
return;
}
const groupValues = groupKey.split('||').slice(0, -1); // Remove last empty string
const groupId = groupKey.replace(/\|\|/g, '_');
// Create group header row
const groupHeader = document.createElement('tr');
groupHeader.classList.add('group-header');
groupHeader.setAttribute('data-group-id', groupId);
const toggleCell = document.createElement('td');
toggleCell.colSpan = this.table.columnsCount();
toggleCell.innerHTML = `<span class="toggle-group">▼</span> Group: ${groupValues.join(', ')}`;
groupHeader.appendChild(toggleCell);
// Add click event to toggle group
groupHeader.addEventListener('click', () => {
const toggleSpan = toggleCell.querySelector('.toggle-group');
const isCollapsed = toggleSpan.textContent === '▼';
toggleSpan.textContent = isCollapsed ? '▶' : '▼';
const groupRows = tbody.querySelectorAll(`tr[data-parent-group-id="${groupId}"]`);
groupRows.forEach(gr => {
gr.classList.toggle('hidden', isCollapsed);
});
});
tbody.appendChild(groupHeader);
// Append group rows
groupRows.forEach(row => {
row.style.display = ''; // Ensure row is visible
row.setAttribute('data-parent-group-id', groupId);
tbody.appendChild(row);
});
});
}
}
// Define the custom element
customElements.define('draggable-table', DraggableTable, {
extends: 'table'
});
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment