Skip to content

Instantly share code, notes, and snippets.

@OnurGumus
Last active October 25, 2024 09:41
Show Gist options
  • Save OnurGumus/cec22dea3bb9ec90f5b6b3de201399ed to your computer and use it in GitHub Desktop.
Save OnurGumus/cec22dea3bb9ec90f5b6b3de201399ed to your computer and use it in GitHub Desktop.
grid
/* Define the typography layer */
@layer typography {
/* Define the fonts layer */
@layer fonts {
/* Example of @font-face declarations */
@font-face {
font-family: "Inter";
src: url("/fonts/Inter-Regular.woff2") format("woff2"),
url("/fonts/Inter-Regular.woff") format("woff");
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Inter";
src: url("/fonts/Inter-Bold.woff2") format("woff2"),
url("/fonts/Inter-Bold.woff") format("woff");
font-weight: 600;
font-style: normal;
font-display: swap;
}
/* Add more font-face declarations as needed */
}
/* Base Typography Styles */
body {
font-family: "Inter", sans-serif;
font-size: 1rem;
line-height: 1.5;
font-weight: 400;
/* Avoid color properties here */
}
h1, h2, h3, h4, h5, h6 {
font-family: "Inter", sans-serif;
font-weight: 600;
line-height: 1.2;
/* Avoid color properties here */
}
p, span, a, button {
font-family: "Inter", sans-serif;
font-size: 1rem;
line-height: 1.5;
/* Avoid color properties here */
}
/* Additional typography styles can be added here */
}
/* Define the animation layer */
@layer animation {
/* Keyframes */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-0.3125rem);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Animation Properties */
.group-tag {
animation: fadeIn var(--transition-duration);
}
/* Transition Properties */
table.draggable-table th,
table.draggable-table td {
transition:
background-color var(--transition-duration),
color var(--transition-duration),
opacity var(--transition-duration);
}
table.draggable-table th.sorted::after {
transition: transform var(--transition-duration);
}
.toggle-group {
transition: transform var(--transition-duration);
}
#grouping-area {
transition:
border-color var(--transition-duration),
background-color var(--transition-duration);
}
th.sorted::after,
th.sort-asc::after,
th.sort-desc::after {
transition: transform var(--transition-duration);
}
/* Additional transition properties can be added here as needed */
}
/* Combine all layers in the desired order */
@layer typography, animation;
/* Root Variables */
:root {
/* Color variables excluded from typography and fonts layers */
--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 {
touch-action: none;
border-collapse: collapse;
inline-size: 100%;
user-select: none;
-webkit-user-select: none; /* Prevent text selection during dragging */
table-layout: fixed; /* Enable fixed table layout for column resizing */
color: var(--font-color);
background-color: var(--background-color);
border-radius: 0.5rem;
overflow: hidden;
box-shadow: 0 0.25rem 1.25rem rgba(0, 0, 0, 0.1);
scrollbar-width: thin;
scrollbar-color: var(--primary-color) rgba(0, 0, 0, 0.1);
/* Scrollbar styling for WebKit browsers */
&::-webkit-scrollbar {
height: 0.5rem;
width: 0.5rem;
}
&::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.1);
}
&::-webkit-scrollbar-thumb {
background-color: var(--primary-color);
border-radius: 0.25rem;
border: 0.125rem solid rgba(0, 0, 0, 0.1);
}
thead {
background: linear-gradient(to right, var(--primary-color), var(--secondary-color));
color: #fff;
touch-action: none;
}
tbody {
tr:hover {
background-color: var(--hover-color);
}
}
th,
td {
touch-action: none;
border: 0.0625rem solid var(--border-color);
padding: 0.75rem;
position: relative;
cursor: pointer;
overflow: hidden; /* Prevent content overflow when resizing */
word-wrap: break-word;
/* Transition properties moved to animation layer */
&:hover {
background-color: var(--hover-color);
}
&.selected {
background-color: var(--selected-color);
}
&.dragging {
opacity: 0.5;
}
}
th {
position: sticky;
top: 0;
background: inherit;
z-index: 2;
&.sorted::after {
content: "";
display: inline-block;
margin-inline-start: 0.5rem;
border: 0.375rem solid transparent;
border-top-color: var(--font-color);
transform: rotate(0deg);
/* Transition property moved to animation layer */
}
&.sort-asc::after {
transform: rotate(180deg);
}
&.sort-desc::after {
transform: rotate(0deg);
}
.filter-container {
display: flex;
flex-direction: column;
margin-block-start: 0.25rem;
.filter-select {
margin-block-end: 0.25rem;
padding: 0.25rem;
font-size: 0.75rem;
border-radius: 0.25rem;
border: 0.0625rem solid var(--border-color);
background-color: #fff;
color: var(--font-color);
}
.filter-input {
inline-size: 100%;
padding: 0.375rem;
font-size: 0.75rem;
border-radius: 0.25rem;
border: 0.0625rem solid var(--border-color);
background-color: #fff;
color: var(--font-color);
box-sizing: border-box;
}
}
}
}
/* Focused cell */
.focused {
outline: 0.125rem solid var(--secondary-color);
outline-offset: -0.125rem;
}
/* Column resizing */
.column-resizer {
position: absolute;
inset-inline-end: 0;
inset-block-start: 0;
inset-block-end: 0;
inline-size: 0.3125rem;
cursor: col-resize;
user-select: none;
}
/* Grouping Area Styles */
#grouping-area {
border: 0.125rem dashed var(--border-color);
padding: 0.625rem;
margin-block-end: 0.625rem;
min-block-size: 2.5rem;
display: flex;
align-items: center;
gap: 0.625rem;
border-radius: 0.5rem;
background-color: #fafafa;
/* Transition properties moved to animation layer */
&.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: 1rem;
padding: 0.375rem 0.75rem;
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
/* Animation property moved to animation layer */
.remove-group {
cursor: pointer;
color: #fff;
font-weight: bold;
&:hover {
color: #e74c3c;
/* Transition property moved to animation layer */
}
}
}
}
/* Group Header Styles */
.group-header {
background-color: rgba(52, 152, 219, 0.1);
cursor: pointer;
font-weight: bold;
/* Transition property moved to animation layer */
&:hover {
background-color: rgba(52, 152, 219, 0.15);
}
.toggle-group {
margin-inline-end: 0.5rem;
cursor: pointer;
font-weight: normal;
/* Transition property moved to animation layer */
&.collapsed::before {
content: "►";
display: inline-block;
margin-inline-end: 0.25rem;
}
&.expanded::before {
content: "▼";
display: inline-block;
margin-inline-end: 0.25rem;
}
}
}
/* Hidden rows */
.hidden {
display: none;
}
/* Responsive design */
@media (max-width: 48rem) { /* 768px */
table.draggable-table {
th,
td {
padding: 0.5rem;
}
.group-tag {
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
}
}
}
<script src="//unpkg.com/@ungap/custom-elements/es.js"></script>
<!-- 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="">-- Select Operator --</option>
<option value="contains">Contains</option>
<option value="startswith">Starts With</option>
<option value="endswith">Ends With</option>
<option value="equal">Equal</option>
<option value="notequal">Not Equal</option>
<option value="greaterthan">Greater Than</option>
<option value="lessthan">Less Than</option>
<option value="greaterequal">Greater or Equal Than</option>
<option value="lessequal">Less or Equal Than</option>
<option value="regex">Regex</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="">-- Select Operator --</option>
<option value="contains">Contains</option>
<option value="startswith">Starts With</option>
<option value="endswith">Ends With</option>
<option value="equal">Equal</option>
<option value="notequal">Not Equal</option>
<option value="greaterthan">Greater Than</option>
<option value="lessthan">Less Than</option>
<option value="greaterequal">Greater or Equal Than</option>
<option value="lessequal">Less or Equal Than</option>
<option value="regex">Regex</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="">-- Select Operator --</option>
<option value="contains">Contains</option>
<option value="startswith">Starts With</option>
<option value="endswith">Ends With</option>
<option value="equal">Equal</option>
<option value="notequal">Not Equal</option>
<option value="greaterthan">Greater Than</option>
<option value="lessthan">Less Than</option>
<option value="greaterequal">Greater or Equal Than</option>
<option value="lessequal">Less or Equal Than</option>
<option value="regex">Regex</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="">-- Select Operator --</option>
<option value="contains">Contains</option>
<option value="startswith">Starts With</option>
<option value="endswith">Ends With</option>
<option value="equal">Equal</option>
<option value="notequal">Not Equal</option>
<option value="greaterthan">Greater Than</option>
<option value="lessthan">Less Than</option>
<option value="greaterequal">Greater or Equal Than</option>
<option value="lessequal">Less or Equal Than</option>
<option value="regex">Regex</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>
<h2> Features </h2>
<ul>
<li>Filter data</li>
<li>Resize columns</li>
<li>Re order columns</li>
<li>Multi column group by</li>
<li>Sort asc, desc, unsort</li>
<li>Navigate focus with arrow keys</li>
<li>Render from server</li>
<li>Add remove data with plain DOM API</li>
<li>Cell selection with dragging or SHIFT Arrow keys</li>
<li>Copy selection and paste to excel</li>
<li>Mobile and touch friendly</li>
<li>Modular code & easy to customize</li>
<li>Cross browser </li>
</ul>
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.isPointerDown = false;
this.startCell = null;
this.endCell = null;
this.selectedCells = new Set();
// Bind methods to maintain the correct 'this' context
this.handlePointerDown = this.handlePointerDown.bind(this);
this.handlePointerMove = this.handlePointerMove.bind(this);
this.handlePointerUp = this.handlePointerUp.bind(this);
this.handleCopy = this.handleCopy.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
this.handleDocumentPointerDown = this.handleDocumentPointerDown.bind(this);
}
connectedCallback() {
// Use pointer events instead of mouse events
this.table.addEventListener("pointerdown", this.handlePointerDown);
// No need to attach pointermove to the table
document.addEventListener("pointerup", this.handlePointerUp);
document.addEventListener("copy", this.handleCopy);
// Add event listeners for Escape key and clicks outside tbody
document.addEventListener("keydown", this.handleKeyDown);
document.addEventListener("pointerdown", this.handleDocumentPointerDown);
}
disconnectedCallback() {
this.table.removeEventListener("pointerdown", this.handlePointerDown);
document.removeEventListener("pointermove", this.handlePointerMove);
document.removeEventListener("pointerup", this.handlePointerUp);
document.removeEventListener("copy", this.handleCopy);
// Remove event listeners
document.removeEventListener("keydown", this.handleKeyDown);
document.removeEventListener("pointerdown", this.handleDocumentPointerDown);
}
handlePointerDown(event) {
// Only primary button (usually left button)
if (event.button !== 0) return;
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.isPointerDown = true;
this.clearSelection();
this.startCell = cell;
this.endCell = cell;
this.updateSelection();
// Attach pointermove listener to the document for global tracking
document.addEventListener("pointermove", this.handlePointerMove);
}
}
handlePointerMove(event) {
if (this.table.isResizing || this.table.isDraggingColumns) return; // Prevent selection during resizing or dragging
if (!this.isPointerDown) return;
// Get the element under the pointer
const element = document.elementFromPoint(event.clientX, event.clientY);
const cell = element ? element.closest("td, th") : null;
if (
cell &&
!cell.parentElement.classList.contains("filter-row") &&
(cell.tagName === "TD" || cell.tagName === "TH")
) {
if (cell !== this.endCell) {
this.endCell = cell;
this.updateSelection();
}
}
}
handlePointerUp(event) {
if (this.isPointerDown) {
this.isPointerDown = false;
// Remove pointermove listener as the drag operation is complete
document.removeEventListener("pointermove", this.handlePointerMove);
}
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();
}
}
}
handleDocumentPointerDown(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();
// Do not reset this.startCell and this.endCell here
}
setStartAndEndCell(cell) {
this.clearSelection();
this.startCell = cell;
this.endCell = cell;
this.updateSelection();
}
setEndCell(cell) {
if (!this.startCell) {
this.setStartAndEndCell(cell);
return;
}
this.endCell = cell;
this.updateSelection();
}
updateSelection() {
if (!this.startCell || !this.endCell) return;
// Remove previous selection without clearing startCell and endCell
this.selectedCells.forEach((cell) => cell.classList.remove("selected"));
this.selectedCells.clear();
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 to maintain the correct 'this' context
this.handleResizePointerDown = this.handleResizePointerDown.bind(this);
this.handleResizePointerMove = this.handleResizePointerMove.bind(this);
this.handleResizePointerUp = this.handleResizePointerUp.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) {
// Use pointerdown instead of mousedown
resizer.addEventListener("pointerdown", this.handleResizePointerDown);
}
});
}
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("pointerdown", this.handleResizePointerDown);
}
});
// Clean up any remaining event listeners on the document
document.removeEventListener("pointermove", this.handleResizePointerMove);
document.removeEventListener("pointerup", this.handleResizePointerUp);
document.removeEventListener("pointercancel", this.handleResizePointerUp);
}
handleResizePointerDown(event) {
event.preventDefault();
event.stopPropagation(); // Prevent the event from bubbling up to the header
this.isResizing = true;
this.table.isResizing = true;
// Identify the column being resized
this.resizingColumn = event.target.closest("th");
if (!this.resizingColumn) return;
this.startX = event.pageX;
this.startWidth = this.resizingColumn.offsetWidth;
// Capture the pointer to ensure all subsequent pointer events are received
this.resizingColumn.setPointerCapture(event.pointerId);
// Add event listeners for pointermove and pointerup to the document
document.addEventListener("pointermove", this.handleResizePointerMove);
document.addEventListener("pointerup", this.handleResizePointerUp);
document.addEventListener("pointercancel", this.handleResizePointerUp);
}
handleResizePointerMove(event) {
if (!this.isResizing) return;
const deltaX = event.pageX - this.startX;
const newWidth = this.startWidth + deltaX;
if (newWidth > 30) { // Ensure a minimum column width
this.resizingColumn.style.width = `${newWidth}px`;
}
}
handleResizePointerUp(event) {
if (this.isResizing) {
this.isResizing = false;
this.table.isResizing = false;
this.resizingColumn = null;
this.table.preventClick = true; // Retain this line as per original functionality
// Release pointer capture
event.target.releasePointerCapture(event.pointerId);
// Remove the event listeners from the document
document.removeEventListener("pointermove", this.handleResizePointerMove);
document.removeEventListener("pointerup", this.handleResizePointerUp);
document.removeEventListener("pointercancel", this.handleResizePointerUp);
}
}
}
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();
// Initialize filters array if not present
if (!this.table.filters) {
this.table.filters = [];
}
// Update the filter for the specific column
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 && filter.value) {
const cellText = row.cells[index].innerText.trim();
const filterValue = filter.value;
switch (filter.operator) {
case "contains":
if (!cellText.toLowerCase().includes(filterValue.toLowerCase())) {
visible = false;
}
break;
case "startswith":
if (!cellText.toLowerCase().startsWith(filterValue.toLowerCase())) {
visible = false;
}
break;
case "endswith":
if (!cellText.toLowerCase().endsWith(filterValue.toLowerCase())) {
visible = false;
}
break;
case "equal":
if (cellText.toLowerCase() !== filterValue.toLowerCase()) {
visible = false;
}
break;
case "notequal":
if (cellText.toLowerCase() === filterValue.toLowerCase()) {
visible = false;
}
break;
case "greaterthan":
const cellNumGT = parseFloat(cellText);
const filterNumGT = parseFloat(filterValue);
if (
isNaN(cellNumGT) ||
isNaN(filterNumGT) ||
cellNumGT <= filterNumGT
) {
visible = false;
}
break;
case "lessthan":
const cellNumLT = parseFloat(cellText);
const filterNumLT = parseFloat(filterValue);
if (
isNaN(cellNumLT) ||
isNaN(filterNumLT) ||
cellNumLT >= filterNumLT
) {
visible = false;
}
break;
case "greaterequal":
const cellNumGE = parseFloat(cellText);
const filterNumGE = parseFloat(filterValue);
if (
isNaN(cellNumGE) ||
isNaN(filterNumGE) ||
cellNumGE < filterNumGE
) {
visible = false;
}
break;
case "lessequal":
const cellNumLE = parseFloat(cellText);
const filterNumLE = parseFloat(filterValue);
if (
isNaN(cellNumLE) ||
isNaN(filterNumLE) ||
cellNumLE > filterNumLE
) {
visible = false;
}
break;
case "regex":
try {
const regex = new RegExp(filterValue, 'i'); // 'i' for case-insensitive
if (!regex.test(cellText)) {
visible = false;
}
} catch (e) {
console.error("Invalid regex pattern:", filterValue);
visible = false;
}
break;
default:
break;
}
}
});
row.style.display = visible ? "" : "none";
});
// Re-apply grouping after filtering
if (this.table.groupedColumns && 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;
// History buffer
this.history = [];
this.currentIndex = -1; // Points to the current position in history
this.maxHistory = 1000; // Increased history size to 1000
// Bind methods
this.handleKeyDown = this.handleKeyDown.bind(this);
this.handleCellClick = this.handleCellClick.bind(this);
this.handleDocumentMouseDown = this.handleDocumentMouseDown.bind(this);
}
connectedCallback() {
// Add event listeners
this.table.addEventListener("keydown", this.handleKeyDown, true); // Use capture phase
this.table.addEventListener("click", this.handleCellClick);
document.addEventListener("mousedown", this.handleDocumentMouseDown);
// Ensure the table can receive focus by setting tabindex
if (!this.table.hasAttribute("tabindex")) {
this.table.setAttribute("tabindex", "0");
}
// Make all cells focusable by setting tabindex
this.makeCellsFocusable();
}
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;
const isShift = event.shiftKey; // Detect if Shift key is pressed
const isCtrl = event.ctrlKey || event.metaKey; // Support Command key on Mac
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;
}
// Handle Undo/Redo
if (isCtrl && key === "ArrowLeft") {
event.preventDefault();
this.undo();
return;
}
if (isCtrl && key === "ArrowRight") {
event.preventDefault();
this.redo();
return;
}
// Only handle arrow keys for navigation
if (!["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(key)) {
return;
}
// Prevent default scrolling behavior
event.preventDefault();
if (!this.currentCell) {
// If no cell is currently focused, focus the first visible cell in tbody
const firstVisibleCell = this.findFirstVisibleCell();
if (firstVisibleCell) {
this.setCurrentCell(firstVisibleCell);
}
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.setCurrentCell(newCell, isShift);
}
}
}
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.setCurrentCell(cell, event.shiftKey);
}
}
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();
}
}
}
setCurrentCell(cell, isShift = false) {
if (this.currentCell !== cell) {
this.currentCell = cell;
this.focusCell(cell);
if (this.table.selectionManager) {
if (isShift && this.table.selectionManager.startCell) {
// If Shift is held, update the endCell to extend the selection
this.table.selectionManager.setEndCell(cell);
} else {
// If Shift is not held, clear selection and set new start and end cells
this.table.selectionManager.setStartAndEndCell(cell);
}
}
// Update history only if cell is in tbody and visible
if (this.isCellInTbody(cell) && this.isCellVisible(cell)) {
this.addToHistory(cell);
}
}
}
addToHistory(cell) {
// If we've undone some steps and then navigate, truncate the redo history
if (this.currentIndex < this.history.length - 1) {
this.history = this.history.slice(0, this.currentIndex + 1);
}
// Avoid adding duplicate consecutive entries
if (this.history[this.history.length - 1] !== cell) {
this.history.push(cell);
this.currentIndex++;
}
// Ensure the history doesn't exceed maxHistory
if (this.history.length > this.maxHistory) {
this.history.shift(); // Remove the oldest entry
this.currentIndex--;
}
}
undo() {
// If current cell is not in tbody or is hidden, jump to the last visible cell in history
if (!this.isCellInTbody(this.currentCell) || !this.isCellVisible(this.currentCell)) {
for (let i = this.history.length - 1; i >= 0; i--) {
const cell = this.history[i];
if (this.isCellVisible(cell)) {
this.currentIndex = i;
this.currentCell = cell;
this.focusCell(cell);
if (this.table.selectionManager) {
this.table.selectionManager.setStartAndEndCell(cell);
}
return;
}
}
// If no visible cell found, focus the first visible cell
const firstVisibleCell = this.findFirstVisibleCell();
if (firstVisibleCell) {
this.currentIndex = this.history.indexOf(firstVisibleCell);
this.currentCell = firstVisibleCell;
this.focusCell(firstVisibleCell);
if (this.table.selectionManager) {
this.table.selectionManager.setStartAndEndCell(firstVisibleCell);
}
}
return;
}
// Current cell is in tbody and visible, move back to find the previous visible cell
for (let i = this.currentIndex - 1; i >= 0; i--) {
const cell = this.history[i];
if (this.isCellVisible(cell)) {
this.currentIndex = i;
this.currentCell = cell;
this.focusCell(cell);
if (this.table.selectionManager) {
this.table.selectionManager.setStartAndEndCell(cell);
}
return;
}
}
// No previous visible cell found, focus the first visible cell
const firstVisibleCell = this.findFirstVisibleCell();
if (firstVisibleCell) {
this.currentIndex = this.history.indexOf(firstVisibleCell);
this.currentCell = firstVisibleCell;
this.focusCell(firstVisibleCell);
if (this.table.selectionManager) {
this.table.selectionManager.setStartAndEndCell(firstVisibleCell);
}
}
}
redo() {
// If current cell is not in tbody or is hidden, jump to the first visible cell in history
if (!this.isCellInTbody(this.currentCell) || !this.isCellVisible(this.currentCell)) {
for (let i = 0; i < this.history.length; i++) {
const cell = this.history[i];
if (this.isCellVisible(cell)) {
this.currentIndex = i;
this.currentCell = cell;
this.focusCell(cell);
if (this.table.selectionManager) {
this.table.selectionManager.setStartAndEndCell(cell);
}
return;
}
}
// If no visible cell found, do nothing
return;
}
// Current cell is in tbody and visible, move forward to find the next visible cell
for (let i = this.currentIndex + 1; i < this.history.length; i++) {
const cell = this.history[i];
if (this.isCellVisible(cell)) {
this.currentIndex = i;
this.currentCell = cell;
this.focusCell(cell);
if (this.table.selectionManager) {
this.table.selectionManager.setStartAndEndCell(cell);
}
return;
}
}
// No next visible cell found, do nothing
}
clearFocus() {
if (this.currentCell) {
// Remove focus class and tabindex
this.currentCell.classList.remove("focused");
this.currentCell.removeAttribute("tabindex");
this.currentCell.blur();
this.currentCell = null;
}
}
focusCell(cell) {
if (!cell) return; // Prevent errors if cell is undefined
// 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();
// Scroll cell into view if necessary
cell.scrollIntoView({
block: "nearest",
inline: "nearest"
});
}
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
}
makeCellsFocusable() {
// Make all cells focusable by setting tabindex
const cells = this.table.querySelectorAll("td, th");
cells.forEach((cell) => {
cell.setAttribute("tabindex", "-1"); // Initially not focusable via tab
});
}
isCellInTbody(cell) {
const tbody = this.table.querySelector("tbody");
return cell && tbody.contains(cell);
}
isCellVisible(cell) {
return cell && this.table.contains(cell) && cell.offsetParent !== null;
}
findFirstVisibleCell() {
for (let i = 0; i < this.history.length; i++) {
const cell = this.history[i];
if (this.isCellVisible(cell)) {
return cell;
}
}
// Optionally, find the first visible cell in tbody
const tbody = this.table.querySelector("tbody");
if (tbody) {
const firstRow = tbody.querySelector("tr:not(.filter-row)");
if (firstRow) {
const firstCell = firstRow.cells[0];
if (this.isCellVisible(firstCell)) {
return firstCell;
}
}
}
return null;
}
}
class GroupingManager {
constructor(table) {
this.table = table;
// 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 = "";
// Remove 'hidden' class from all rows to ensure they are visible
this.table.originalRows.forEach((row) => {
row.classList.remove("hidden"); // **Added Line**
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 based on filters
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"
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment