First version of DataGridQuery. Fixed scrollbar issue

This commit is contained in:
2026-01-23 21:26:19 +01:00
parent 872d110f07
commit 191ead1c89
6 changed files with 432 additions and 331 deletions

View File

@@ -636,7 +636,7 @@ function updateTabs(controllerId) {
// Add key to current pressed keys
KeyboardRegistry.currentKeys.add(key);
console.debug("Received key", key);
// console.debug("Received key", key);
// Create a snapshot of current keyboard state
const snapshot = new Set(KeyboardRegistry.currentKeys);
@@ -671,7 +671,7 @@ function updateTabs(controllerId) {
if (!currentNode) {
// No match in this tree, continue to next element
console.debug("No match in tree for event", key);
// console.debug("No match in tree for event", key);
continue;
}
@@ -1289,7 +1289,7 @@ function updateTabs(controllerId) {
return;
}
console.debug("Right-click on registered element", elementId);
//console.debug("Right-click on registered element", elementId);
// For right-click, clicked_inside is always true (we only trigger if clicked on element)
const clickedInside = true;
@@ -1322,7 +1322,7 @@ function updateTabs(controllerId) {
if (!currentNode) {
// No match in this tree
console.debug("No match in tree for right-click");
//console.debug("No match in tree for right-click");
// Clear history for invalid sequences
MouseRegistry.snapshotHistory = [];
return;
@@ -1518,6 +1518,14 @@ function initDataGridScrollbars(gridId) {
return;
}
// Cleanup previous listeners if any
if (wrapper._scrollbarAbortController) {
wrapper._scrollbarAbortController.abort();
}
wrapper._scrollbarAbortController = new AbortController();
const signal = wrapper._scrollbarAbortController.signal;
const verticalScrollbar = wrapper.querySelector(".dt2-scrollbars-vertical");
const verticalWrapper = wrapper.querySelector(".dt2-scrollbars-vertical-wrapper");
const horizontalScrollbar = wrapper.querySelector(".dt2-scrollbars-horizontal");
@@ -1577,7 +1585,6 @@ function initDataGridScrollbars(gridId) {
};
// PHASE 2: Calculate all values
const contentWidth = Math.max(metrics.headerScrollWidth, metrics.bodyScrollWidth);
// Visibility
@@ -1649,7 +1656,7 @@ function initDataGridScrollbars(gridId) {
dragStartY = e.clientY;
dragStartScrollTop = cachedBodyScrollTop;
wrapper.setAttribute("mf-no-tooltip", "");
});
}, { signal });
// Horizontal scrollbar mousedown
horizontalScrollbar.addEventListener("mousedown", (e) => {
@@ -1657,7 +1664,7 @@ function initDataGridScrollbars(gridId) {
dragStartX = e.clientX;
dragStartScrollLeft = cachedTableScrollLeft;
wrapper.setAttribute("mf-no-tooltip", "");
});
}, { signal });
// Consolidated mousemove listener
document.addEventListener("mousemove", (e) => {
@@ -1688,7 +1695,7 @@ function initDataGridScrollbars(gridId) {
});
}
}
});
}, { signal });
// Consolidated mouseup listener
document.addEventListener("mouseup", () => {
@@ -1699,7 +1706,7 @@ function initDataGridScrollbars(gridId) {
isDraggingHorizontal = false;
wrapper.removeAttribute("mf-no-tooltip");
}
});
}, { signal });
// Wheel scrolling - OPTIMIZED with RAF throttling
let rafScheduledWheel = false;
@@ -1737,7 +1744,7 @@ function initDataGridScrollbars(gridId) {
event.preventDefault();
};
wrapper.addEventListener("wheel", handleWheelScrolling, {passive: false});
wrapper.addEventListener("wheel", handleWheelScrolling, {passive: false, signal});
// Initialize scrollbars with single batched update
updateScrollbars();
@@ -1752,109 +1759,109 @@ function initDataGridScrollbars(gridId) {
updateScrollbars();
});
}
});
}, { signal });
}
function makeDatagridColumnsResizable(datagridId) {
console.debug("makeResizable on element " + datagridId);
//console.debug("makeResizable on element " + datagridId);
const tableId = 't_' + datagridId;
const table = document.getElementById(tableId);
const resizeHandles = table.querySelectorAll('.dt2-resize-handle');
const MIN_WIDTH = 30; // Prevent columns from becoming too narrow
const tableId = 't_' + datagridId;
const table = document.getElementById(tableId);
const resizeHandles = table.querySelectorAll('.dt2-resize-handle');
const MIN_WIDTH = 30; // Prevent columns from becoming too narrow
// Attach event listeners using delegation
resizeHandles.forEach(handle => {
handle.addEventListener('mousedown', onStartResize);
handle.addEventListener('touchstart', onStartResize, {passive: false});
handle.addEventListener('dblclick', onDoubleClick); // Reset column width
// Attach event listeners using delegation
resizeHandles.forEach(handle => {
handle.addEventListener('mousedown', onStartResize);
handle.addEventListener('touchstart', onStartResize, {passive: false});
handle.addEventListener('dblclick', onDoubleClick); // Reset column width
});
let resizingState = null; // Maintain resizing state information
function onStartResize(event) {
event.preventDefault(); // Prevent unintended selections
const isTouch = event.type === 'touchstart';
const startX = isTouch ? event.touches[0].pageX : event.pageX;
const handle = event.target;
const cell = handle.parentElement;
const colIndex = cell.getAttribute('data-col');
const commandId = handle.dataset.commandId;
const cells = table.querySelectorAll(`.dt2-cell[data-col="${colIndex}"]`);
// Store initial state
const startWidth = cell.offsetWidth + 8;
resizingState = {startX, startWidth, colIndex, commandId, cells};
// Attach event listeners for resizing
document.addEventListener(isTouch ? 'touchmove' : 'mousemove', onResize);
document.addEventListener(isTouch ? 'touchend' : 'mouseup', onStopResize);
}
function onResize(event) {
if (!resizingState) {
return;
}
const isTouch = event.type === 'touchmove';
const currentX = isTouch ? event.touches[0].pageX : event.pageX;
const {startX, startWidth, cells} = resizingState;
// Calculate new width and apply constraints
const newWidth = Math.max(MIN_WIDTH, startWidth + (currentX - startX));
cells.forEach(cell => {
cell.style.width = `${newWidth}px`;
});
}
function onStopResize(event) {
if (!resizingState) {
return;
}
const {colIndex, commandId, cells} = resizingState;
const finalWidth = cells[0].offsetWidth;
// Send width update to server via HTMX
if (commandId) {
htmx.ajax('POST', '/myfasthtml/commands', {
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
swap: 'none',
values: {
c_id: commandId,
col_id: colIndex,
width: finalWidth
}
});
}
// Clean up
resizingState = null;
document.removeEventListener('mousemove', onResize);
document.removeEventListener('mouseup', onStopResize);
document.removeEventListener('touchmove', onResize);
document.removeEventListener('touchend', onStopResize);
}
function onDoubleClick(event) {
const handle = event.target;
const cell = handle.parentElement;
const colIndex = cell.getAttribute('data-col');
const cells = table.querySelectorAll(`.dt2-cell[data-col="${colIndex}"]`);
// Reset column width
cells.forEach(cell => {
cell.style.width = ''; // Use CSS default width
});
let resizingState = null; // Maintain resizing state information
function onStartResize(event) {
event.preventDefault(); // Prevent unintended selections
const isTouch = event.type === 'touchstart';
const startX = isTouch ? event.touches[0].pageX : event.pageX;
const handle = event.target;
const cell = handle.parentElement;
const colIndex = cell.getAttribute('data-col');
const commandId = handle.dataset.commandId;
const cells = table.querySelectorAll(`.dt2-cell[data-col="${colIndex}"]`);
// Store initial state
const startWidth = cell.offsetWidth + 8;
resizingState = {startX, startWidth, colIndex, commandId, cells};
// Attach event listeners for resizing
document.addEventListener(isTouch ? 'touchmove' : 'mousemove', onResize);
document.addEventListener(isTouch ? 'touchend' : 'mouseup', onStopResize);
}
function onResize(event) {
if (!resizingState) {
return;
}
const isTouch = event.type === 'touchmove';
const currentX = isTouch ? event.touches[0].pageX : event.pageX;
const {startX, startWidth, cells} = resizingState;
// Calculate new width and apply constraints
const newWidth = Math.max(MIN_WIDTH, startWidth + (currentX - startX));
cells.forEach(cell => {
cell.style.width = `${newWidth}px`;
});
}
function onStopResize(event) {
if (!resizingState) {
return;
}
const {colIndex, commandId, cells} = resizingState;
const finalWidth = cells[0].offsetWidth;
// Send width update to server via HTMX
if (commandId) {
htmx.ajax('POST', '/myfasthtml/commands', {
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
swap: 'none',
values: {
c_id: commandId,
col_id: colIndex,
width: finalWidth
}
});
}
// Clean up
resizingState = null;
document.removeEventListener('mousemove', onResize);
document.removeEventListener('mouseup', onStopResize);
document.removeEventListener('touchmove', onResize);
document.removeEventListener('touchend', onStopResize);
}
function onDoubleClick(event) {
const handle = event.target;
const cell = handle.parentElement;
const colIndex = cell.getAttribute('data-col');
const cells = table.querySelectorAll(`.dt2-cell[data-col="${colIndex}"]`);
// Reset column width
cells.forEach(cell => {
cell.style.width = ''; // Use CSS default width
});
// Emit reset event
const resetEvent = new CustomEvent('columnReset', {detail: {colIndex}});
table.dispatchEvent(resetEvent);
}
// Emit reset event
const resetEvent = new CustomEvent('columnReset', {detail: {colIndex}});
table.dispatchEvent(resetEvent);
}
}
/**
@@ -1863,84 +1870,84 @@ function makeDatagridColumnsResizable(datagridId) {
* @param {string} gridId - The DataGrid instance ID
*/
function makeDatagridColumnsMovable(gridId) {
const table = document.getElementById(`t_${gridId}`);
const headerRow = document.getElementById(`th_${gridId}`);
const table = document.getElementById(`t_${gridId}`);
const headerRow = document.getElementById(`th_${gridId}`);
if (!table || !headerRow) {
console.error(`DataGrid elements not found for ${gridId}`);
return;
if (!table || !headerRow) {
console.error(`DataGrid elements not found for ${gridId}`);
return;
}
const moveCommandId = headerRow.dataset.moveCommandId;
const headerCells = headerRow.querySelectorAll('.dt2-cell:not(.dt2-col-hidden)');
let sourceColumn = null; // Column being dragged (original position)
let lastMoveTarget = null; // Last column we moved to (for persistence)
let hoverColumn = null; // Current hover target (for delayed move check)
headerCells.forEach(cell => {
cell.setAttribute('draggable', 'true');
// Prevent drag when clicking resize handle
const resizeHandle = cell.querySelector('.dt2-resize-handle');
if (resizeHandle) {
resizeHandle.addEventListener('mousedown', () => cell.setAttribute('draggable', 'false'));
resizeHandle.addEventListener('mouseup', () => cell.setAttribute('draggable', 'true'));
}
const moveCommandId = headerRow.dataset.moveCommandId;
const headerCells = headerRow.querySelectorAll('.dt2-cell:not(.dt2-col-hidden)');
let sourceColumn = null; // Column being dragged (original position)
let lastMoveTarget = null; // Last column we moved to (for persistence)
let hoverColumn = null; // Current hover target (for delayed move check)
headerCells.forEach(cell => {
cell.setAttribute('draggable', 'true');
// Prevent drag when clicking resize handle
const resizeHandle = cell.querySelector('.dt2-resize-handle');
if (resizeHandle) {
resizeHandle.addEventListener('mousedown', () => cell.setAttribute('draggable', 'false'));
resizeHandle.addEventListener('mouseup', () => cell.setAttribute('draggable', 'true'));
}
cell.addEventListener('dragstart', (e) => {
sourceColumn = cell.getAttribute('data-col');
lastMoveTarget = null;
hoverColumn = null;
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', sourceColumn);
cell.classList.add('dt2-dragging');
});
cell.addEventListener('dragenter', (e) => {
e.preventDefault();
const targetColumn = cell.getAttribute('data-col');
hoverColumn = targetColumn;
if (sourceColumn && sourceColumn !== targetColumn) {
// Delay to skip columns when dragging fast
setTimeout(() => {
if (hoverColumn === targetColumn) {
moveColumn(table, sourceColumn, targetColumn);
lastMoveTarget = targetColumn;
}
}, 50);
}
});
cell.addEventListener('dragover', (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
});
cell.addEventListener('drop', (e) => {
e.preventDefault();
// Persist to server
if (moveCommandId && sourceColumn && lastMoveTarget) {
htmx.ajax('POST', '/myfasthtml/commands', {
headers: {"Content-Type": "application/x-www-form-urlencoded"},
swap: 'none',
values: {
c_id: moveCommandId,
source_col_id: sourceColumn,
target_col_id: lastMoveTarget
}
});
}
});
cell.addEventListener('dragend', () => {
headerCells.forEach(c => c.classList.remove('dt2-dragging'));
sourceColumn = null;
lastMoveTarget = null;
hoverColumn = null;
});
cell.addEventListener('dragstart', (e) => {
sourceColumn = cell.getAttribute('data-col');
lastMoveTarget = null;
hoverColumn = null;
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', sourceColumn);
cell.classList.add('dt2-dragging');
});
cell.addEventListener('dragenter', (e) => {
e.preventDefault();
const targetColumn = cell.getAttribute('data-col');
hoverColumn = targetColumn;
if (sourceColumn && sourceColumn !== targetColumn) {
// Delay to skip columns when dragging fast
setTimeout(() => {
if (hoverColumn === targetColumn) {
moveColumn(table, sourceColumn, targetColumn);
lastMoveTarget = targetColumn;
}
}, 50);
}
});
cell.addEventListener('dragover', (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
});
cell.addEventListener('drop', (e) => {
e.preventDefault();
// Persist to server
if (moveCommandId && sourceColumn && lastMoveTarget) {
htmx.ajax('POST', '/myfasthtml/commands', {
headers: {"Content-Type": "application/x-www-form-urlencoded"},
swap: 'none',
values: {
c_id: moveCommandId,
source_col_id: sourceColumn,
target_col_id: lastMoveTarget
}
});
}
});
cell.addEventListener('dragend', () => {
headerCells.forEach(c => c.classList.remove('dt2-dragging'));
sourceColumn = null;
lastMoveTarget = null;
hoverColumn = null;
});
});
}
/**
@@ -1951,67 +1958,67 @@ function makeDatagridColumnsMovable(gridId) {
* @param {string} targetColId - Column ID to move next to
*/
function moveColumn(table, sourceColId, targetColId) {
const ANIMATION_DURATION = 300; // Must match CSS transition duration
const ANIMATION_DURATION = 300; // Must match CSS transition duration
const sourceHeader = table.querySelector(`.dt2-cell[data-col="${sourceColId}"]`);
const targetHeader = table.querySelector(`.dt2-cell[data-col="${targetColId}"]`);
const sourceHeader = table.querySelector(`.dt2-cell[data-col="${sourceColId}"]`);
const targetHeader = table.querySelector(`.dt2-cell[data-col="${targetColId}"]`);
if (!sourceHeader || !targetHeader) return;
if (sourceHeader.classList.contains('dt2-moving')) return; // Animation in progress
if (!sourceHeader || !targetHeader) return;
if (sourceHeader.classList.contains('dt2-moving')) return; // Animation in progress
const headerCells = Array.from(sourceHeader.parentNode.children);
const sourceIdx = headerCells.indexOf(sourceHeader);
const targetIdx = headerCells.indexOf(targetHeader);
const headerCells = Array.from(sourceHeader.parentNode.children);
const sourceIdx = headerCells.indexOf(sourceHeader);
const targetIdx = headerCells.indexOf(targetHeader);
if (sourceIdx === targetIdx) return;
if (sourceIdx === targetIdx) return;
const movingRight = sourceIdx < targetIdx;
const sourceCells = table.querySelectorAll(`.dt2-cell[data-col="${sourceColId}"]`);
const movingRight = sourceIdx < targetIdx;
const sourceCells = table.querySelectorAll(`.dt2-cell[data-col="${sourceColId}"]`);
// Collect cells that need to shift (between source and target)
const cellsToShift = [];
let shiftWidth = 0;
const [startIdx, endIdx] = movingRight
? [sourceIdx + 1, targetIdx]
: [targetIdx, sourceIdx - 1];
// Collect cells that need to shift (between source and target)
const cellsToShift = [];
let shiftWidth = 0;
const [startIdx, endIdx] = movingRight
? [sourceIdx + 1, targetIdx]
: [targetIdx, sourceIdx - 1];
for (let i = startIdx; i <= endIdx; i++) {
const colId = headerCells[i].getAttribute('data-col');
cellsToShift.push(...table.querySelectorAll(`.dt2-cell[data-col="${colId}"]`));
shiftWidth += headerCells[i].offsetWidth;
}
for (let i = startIdx; i <= endIdx; i++) {
const colId = headerCells[i].getAttribute('data-col');
cellsToShift.push(...table.querySelectorAll(`.dt2-cell[data-col="${colId}"]`));
shiftWidth += headerCells[i].offsetWidth;
}
// Calculate animation distances
const sourceWidth = sourceHeader.offsetWidth;
const sourceDistance = movingRight ? shiftWidth : -shiftWidth;
const shiftDistance = movingRight ? -sourceWidth : sourceWidth;
// Calculate animation distances
const sourceWidth = sourceHeader.offsetWidth;
const sourceDistance = movingRight ? shiftWidth : -shiftWidth;
const shiftDistance = movingRight ? -sourceWidth : sourceWidth;
// Animate source column
sourceCells.forEach(cell => {
cell.classList.add('dt2-moving');
cell.style.transform = `translateX(${sourceDistance}px)`;
// Animate source column
sourceCells.forEach(cell => {
cell.classList.add('dt2-moving');
cell.style.transform = `translateX(${sourceDistance}px)`;
});
// Animate shifted columns
cellsToShift.forEach(cell => {
cell.classList.add('dt2-moving');
cell.style.transform = `translateX(${shiftDistance}px)`;
});
// After animation: reset transforms and update DOM
setTimeout(() => {
[...sourceCells, ...cellsToShift].forEach(cell => {
cell.classList.remove('dt2-moving');
cell.style.transform = '';
});
// Animate shifted columns
cellsToShift.forEach(cell => {
cell.classList.add('dt2-moving');
cell.style.transform = `translateX(${shiftDistance}px)`;
// Move source column in DOM
table.querySelectorAll('.dt2-row').forEach(row => {
const sourceCell = row.querySelector(`[data-col="${sourceColId}"]`);
const targetCell = row.querySelector(`[data-col="${targetColId}"]`);
if (sourceCell && targetCell) {
movingRight ? targetCell.after(sourceCell) : targetCell.before(sourceCell);
}
});
// After animation: reset transforms and update DOM
setTimeout(() => {
[...sourceCells, ...cellsToShift].forEach(cell => {
cell.classList.remove('dt2-moving');
cell.style.transform = '';
});
// Move source column in DOM
table.querySelectorAll('.dt2-row').forEach(row => {
const sourceCell = row.querySelector(`[data-col="${sourceColId}"]`);
const targetCell = row.querySelector(`[data-col="${targetColId}"]`);
if (sourceCell && targetCell) {
movingRight ? targetCell.after(sourceCell) : targetCell.before(sourceCell);
}
});
}, ANIMATION_DURATION);
}, ANIMATION_DURATION);
}