First version of DataGridQuery. Fixed scrollbar issue
This commit is contained in:
@@ -636,7 +636,7 @@ function updateTabs(controllerId) {
|
|||||||
|
|
||||||
// Add key to current pressed keys
|
// Add key to current pressed keys
|
||||||
KeyboardRegistry.currentKeys.add(key);
|
KeyboardRegistry.currentKeys.add(key);
|
||||||
console.debug("Received key", key);
|
// console.debug("Received key", key);
|
||||||
|
|
||||||
// Create a snapshot of current keyboard state
|
// Create a snapshot of current keyboard state
|
||||||
const snapshot = new Set(KeyboardRegistry.currentKeys);
|
const snapshot = new Set(KeyboardRegistry.currentKeys);
|
||||||
@@ -671,7 +671,7 @@ function updateTabs(controllerId) {
|
|||||||
|
|
||||||
if (!currentNode) {
|
if (!currentNode) {
|
||||||
// No match in this tree, continue to next element
|
// 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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1289,7 +1289,7 @@ function updateTabs(controllerId) {
|
|||||||
return;
|
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)
|
// For right-click, clicked_inside is always true (we only trigger if clicked on element)
|
||||||
const clickedInside = true;
|
const clickedInside = true;
|
||||||
@@ -1322,7 +1322,7 @@ function updateTabs(controllerId) {
|
|||||||
|
|
||||||
if (!currentNode) {
|
if (!currentNode) {
|
||||||
// No match in this tree
|
// 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
|
// Clear history for invalid sequences
|
||||||
MouseRegistry.snapshotHistory = [];
|
MouseRegistry.snapshotHistory = [];
|
||||||
return;
|
return;
|
||||||
@@ -1518,6 +1518,14 @@ function initDataGridScrollbars(gridId) {
|
|||||||
return;
|
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 verticalScrollbar = wrapper.querySelector(".dt2-scrollbars-vertical");
|
||||||
const verticalWrapper = wrapper.querySelector(".dt2-scrollbars-vertical-wrapper");
|
const verticalWrapper = wrapper.querySelector(".dt2-scrollbars-vertical-wrapper");
|
||||||
const horizontalScrollbar = wrapper.querySelector(".dt2-scrollbars-horizontal");
|
const horizontalScrollbar = wrapper.querySelector(".dt2-scrollbars-horizontal");
|
||||||
@@ -1577,7 +1585,6 @@ function initDataGridScrollbars(gridId) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// PHASE 2: Calculate all values
|
// PHASE 2: Calculate all values
|
||||||
|
|
||||||
const contentWidth = Math.max(metrics.headerScrollWidth, metrics.bodyScrollWidth);
|
const contentWidth = Math.max(metrics.headerScrollWidth, metrics.bodyScrollWidth);
|
||||||
|
|
||||||
// Visibility
|
// Visibility
|
||||||
@@ -1649,7 +1656,7 @@ function initDataGridScrollbars(gridId) {
|
|||||||
dragStartY = e.clientY;
|
dragStartY = e.clientY;
|
||||||
dragStartScrollTop = cachedBodyScrollTop;
|
dragStartScrollTop = cachedBodyScrollTop;
|
||||||
wrapper.setAttribute("mf-no-tooltip", "");
|
wrapper.setAttribute("mf-no-tooltip", "");
|
||||||
});
|
}, { signal });
|
||||||
|
|
||||||
// Horizontal scrollbar mousedown
|
// Horizontal scrollbar mousedown
|
||||||
horizontalScrollbar.addEventListener("mousedown", (e) => {
|
horizontalScrollbar.addEventListener("mousedown", (e) => {
|
||||||
@@ -1657,7 +1664,7 @@ function initDataGridScrollbars(gridId) {
|
|||||||
dragStartX = e.clientX;
|
dragStartX = e.clientX;
|
||||||
dragStartScrollLeft = cachedTableScrollLeft;
|
dragStartScrollLeft = cachedTableScrollLeft;
|
||||||
wrapper.setAttribute("mf-no-tooltip", "");
|
wrapper.setAttribute("mf-no-tooltip", "");
|
||||||
});
|
}, { signal });
|
||||||
|
|
||||||
// Consolidated mousemove listener
|
// Consolidated mousemove listener
|
||||||
document.addEventListener("mousemove", (e) => {
|
document.addEventListener("mousemove", (e) => {
|
||||||
@@ -1688,7 +1695,7 @@ function initDataGridScrollbars(gridId) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
}, { signal });
|
||||||
|
|
||||||
// Consolidated mouseup listener
|
// Consolidated mouseup listener
|
||||||
document.addEventListener("mouseup", () => {
|
document.addEventListener("mouseup", () => {
|
||||||
@@ -1699,7 +1706,7 @@ function initDataGridScrollbars(gridId) {
|
|||||||
isDraggingHorizontal = false;
|
isDraggingHorizontal = false;
|
||||||
wrapper.removeAttribute("mf-no-tooltip");
|
wrapper.removeAttribute("mf-no-tooltip");
|
||||||
}
|
}
|
||||||
});
|
}, { signal });
|
||||||
|
|
||||||
// Wheel scrolling - OPTIMIZED with RAF throttling
|
// Wheel scrolling - OPTIMIZED with RAF throttling
|
||||||
let rafScheduledWheel = false;
|
let rafScheduledWheel = false;
|
||||||
@@ -1737,7 +1744,7 @@ function initDataGridScrollbars(gridId) {
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
};
|
};
|
||||||
|
|
||||||
wrapper.addEventListener("wheel", handleWheelScrolling, {passive: false});
|
wrapper.addEventListener("wheel", handleWheelScrolling, {passive: false, signal});
|
||||||
|
|
||||||
// Initialize scrollbars with single batched update
|
// Initialize scrollbars with single batched update
|
||||||
updateScrollbars();
|
updateScrollbars();
|
||||||
@@ -1752,109 +1759,109 @@ function initDataGridScrollbars(gridId) {
|
|||||||
updateScrollbars();
|
updateScrollbars();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
}, { signal });
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeDatagridColumnsResizable(datagridId) {
|
function makeDatagridColumnsResizable(datagridId) {
|
||||||
console.debug("makeResizable on element " + datagridId);
|
//console.debug("makeResizable on element " + datagridId);
|
||||||
|
|
||||||
const tableId = 't_' + datagridId;
|
const tableId = 't_' + datagridId;
|
||||||
const table = document.getElementById(tableId);
|
const table = document.getElementById(tableId);
|
||||||
const resizeHandles = table.querySelectorAll('.dt2-resize-handle');
|
const resizeHandles = table.querySelectorAll('.dt2-resize-handle');
|
||||||
const MIN_WIDTH = 30; // Prevent columns from becoming too narrow
|
const MIN_WIDTH = 30; // Prevent columns from becoming too narrow
|
||||||
|
|
||||||
// Attach event listeners using delegation
|
// Attach event listeners using delegation
|
||||||
resizeHandles.forEach(handle => {
|
resizeHandles.forEach(handle => {
|
||||||
handle.addEventListener('mousedown', onStartResize);
|
handle.addEventListener('mousedown', onStartResize);
|
||||||
handle.addEventListener('touchstart', onStartResize, {passive: false});
|
handle.addEventListener('touchstart', onStartResize, {passive: false});
|
||||||
handle.addEventListener('dblclick', onDoubleClick); // Reset column width
|
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
|
// Emit reset event
|
||||||
|
const resetEvent = new CustomEvent('columnReset', {detail: {colIndex}});
|
||||||
function onStartResize(event) {
|
table.dispatchEvent(resetEvent);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1863,84 +1870,84 @@ function makeDatagridColumnsResizable(datagridId) {
|
|||||||
* @param {string} gridId - The DataGrid instance ID
|
* @param {string} gridId - The DataGrid instance ID
|
||||||
*/
|
*/
|
||||||
function makeDatagridColumnsMovable(gridId) {
|
function makeDatagridColumnsMovable(gridId) {
|
||||||
const table = document.getElementById(`t_${gridId}`);
|
const table = document.getElementById(`t_${gridId}`);
|
||||||
const headerRow = document.getElementById(`th_${gridId}`);
|
const headerRow = document.getElementById(`th_${gridId}`);
|
||||||
|
|
||||||
if (!table || !headerRow) {
|
if (!table || !headerRow) {
|
||||||
console.error(`DataGrid elements not found for ${gridId}`);
|
console.error(`DataGrid elements not found for ${gridId}`);
|
||||||
return;
|
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;
|
cell.addEventListener('dragstart', (e) => {
|
||||||
const headerCells = headerRow.querySelectorAll('.dt2-cell:not(.dt2-col-hidden)');
|
sourceColumn = cell.getAttribute('data-col');
|
||||||
|
lastMoveTarget = null;
|
||||||
let sourceColumn = null; // Column being dragged (original position)
|
hoverColumn = null;
|
||||||
let lastMoveTarget = null; // Last column we moved to (for persistence)
|
e.dataTransfer.effectAllowed = 'move';
|
||||||
let hoverColumn = null; // Current hover target (for delayed move check)
|
e.dataTransfer.setData('text/plain', sourceColumn);
|
||||||
|
cell.classList.add('dt2-dragging');
|
||||||
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('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
|
* @param {string} targetColId - Column ID to move next to
|
||||||
*/
|
*/
|
||||||
function moveColumn(table, sourceColId, targetColId) {
|
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 sourceHeader = table.querySelector(`.dt2-cell[data-col="${sourceColId}"]`);
|
||||||
const targetHeader = table.querySelector(`.dt2-cell[data-col="${targetColId}"]`);
|
const targetHeader = table.querySelector(`.dt2-cell[data-col="${targetColId}"]`);
|
||||||
|
|
||||||
if (!sourceHeader || !targetHeader) return;
|
if (!sourceHeader || !targetHeader) return;
|
||||||
if (sourceHeader.classList.contains('dt2-moving')) return; // Animation in progress
|
if (sourceHeader.classList.contains('dt2-moving')) return; // Animation in progress
|
||||||
|
|
||||||
const headerCells = Array.from(sourceHeader.parentNode.children);
|
const headerCells = Array.from(sourceHeader.parentNode.children);
|
||||||
const sourceIdx = headerCells.indexOf(sourceHeader);
|
const sourceIdx = headerCells.indexOf(sourceHeader);
|
||||||
const targetIdx = headerCells.indexOf(targetHeader);
|
const targetIdx = headerCells.indexOf(targetHeader);
|
||||||
|
|
||||||
if (sourceIdx === targetIdx) return;
|
if (sourceIdx === targetIdx) return;
|
||||||
|
|
||||||
const movingRight = sourceIdx < targetIdx;
|
const movingRight = sourceIdx < targetIdx;
|
||||||
const sourceCells = table.querySelectorAll(`.dt2-cell[data-col="${sourceColId}"]`);
|
const sourceCells = table.querySelectorAll(`.dt2-cell[data-col="${sourceColId}"]`);
|
||||||
|
|
||||||
// Collect cells that need to shift (between source and target)
|
// Collect cells that need to shift (between source and target)
|
||||||
const cellsToShift = [];
|
const cellsToShift = [];
|
||||||
let shiftWidth = 0;
|
let shiftWidth = 0;
|
||||||
const [startIdx, endIdx] = movingRight
|
const [startIdx, endIdx] = movingRight
|
||||||
? [sourceIdx + 1, targetIdx]
|
? [sourceIdx + 1, targetIdx]
|
||||||
: [targetIdx, sourceIdx - 1];
|
: [targetIdx, sourceIdx - 1];
|
||||||
|
|
||||||
for (let i = startIdx; i <= endIdx; i++) {
|
for (let i = startIdx; i <= endIdx; i++) {
|
||||||
const colId = headerCells[i].getAttribute('data-col');
|
const colId = headerCells[i].getAttribute('data-col');
|
||||||
cellsToShift.push(...table.querySelectorAll(`.dt2-cell[data-col="${colId}"]`));
|
cellsToShift.push(...table.querySelectorAll(`.dt2-cell[data-col="${colId}"]`));
|
||||||
shiftWidth += headerCells[i].offsetWidth;
|
shiftWidth += headerCells[i].offsetWidth;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate animation distances
|
// Calculate animation distances
|
||||||
const sourceWidth = sourceHeader.offsetWidth;
|
const sourceWidth = sourceHeader.offsetWidth;
|
||||||
const sourceDistance = movingRight ? shiftWidth : -shiftWidth;
|
const sourceDistance = movingRight ? shiftWidth : -shiftWidth;
|
||||||
const shiftDistance = movingRight ? -sourceWidth : sourceWidth;
|
const shiftDistance = movingRight ? -sourceWidth : sourceWidth;
|
||||||
|
|
||||||
// Animate source column
|
// Animate source column
|
||||||
sourceCells.forEach(cell => {
|
sourceCells.forEach(cell => {
|
||||||
cell.classList.add('dt2-moving');
|
cell.classList.add('dt2-moving');
|
||||||
cell.style.transform = `translateX(${sourceDistance}px)`;
|
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
|
// Move source column in DOM
|
||||||
cellsToShift.forEach(cell => {
|
table.querySelectorAll('.dt2-row').forEach(row => {
|
||||||
cell.classList.add('dt2-moving');
|
const sourceCell = row.querySelector(`[data-col="${sourceColId}"]`);
|
||||||
cell.style.transform = `translateX(${shiftDistance}px)`;
|
const targetCell = row.querySelector(`[data-col="${targetColId}"]`);
|
||||||
|
if (sourceCell && targetCell) {
|
||||||
|
movingRight ? targetCell.after(sourceCell) : targetCell.before(sourceCell);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
}, ANIMATION_DURATION);
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ from typing import Optional
|
|||||||
import pandas as pd
|
import pandas as pd
|
||||||
from fasthtml.common import NotStr
|
from fasthtml.common import NotStr
|
||||||
from fasthtml.components import *
|
from fasthtml.components import *
|
||||||
|
from pandas import DataFrame
|
||||||
|
|
||||||
from myfasthtml.controls.BaseCommands import BaseCommands
|
from myfasthtml.controls.BaseCommands import BaseCommands
|
||||||
|
from myfasthtml.controls.DataGridQuery import DataGridQuery, DG_QUERY_FILTER
|
||||||
from myfasthtml.controls.datagrid_objects import DataGridColumnState, DataGridRowState, \
|
from myfasthtml.controls.datagrid_objects import DataGridColumnState, DataGridRowState, \
|
||||||
DatagridSelectionState, DataGridHeaderFooterConf, DatagridEditionState
|
DatagridSelectionState, DataGridHeaderFooterConf, DatagridEditionState
|
||||||
from myfasthtml.controls.helpers import mk
|
from myfasthtml.controls.helpers import mk
|
||||||
@@ -102,6 +104,13 @@ class Commands(BaseCommands):
|
|||||||
self._owner.move_column
|
self._owner.move_column
|
||||||
).htmx(target=None)
|
).htmx(target=None)
|
||||||
|
|
||||||
|
def filter(self):
|
||||||
|
return Command("Filter",
|
||||||
|
"Filter Grid",
|
||||||
|
self._owner,
|
||||||
|
self._owner.filter
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class DataGrid(MultipleInstance):
|
class DataGrid(MultipleInstance):
|
||||||
def __init__(self, parent, settings=None, save_state=None, _id=None):
|
def __init__(self, parent, settings=None, save_state=None, _id=None):
|
||||||
@@ -110,11 +119,56 @@ class DataGrid(MultipleInstance):
|
|||||||
self._state = DatagridState(self, save_state=self._settings.save_state)
|
self._state = DatagridState(self, save_state=self._settings.save_state)
|
||||||
self.commands = Commands(self)
|
self.commands = Commands(self)
|
||||||
self.init_from_dataframe(self._state.ne_df, init_state=False) # state comes from DatagridState
|
self.init_from_dataframe(self._state.ne_df, init_state=False) # state comes from DatagridState
|
||||||
|
self._datagrid_filter = DataGridQuery(self)
|
||||||
|
self._datagrid_filter.bind_command("QueryChanged", self.commands.filter())
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _df(self):
|
def _df(self):
|
||||||
return self._state.ne_df
|
return self._state.ne_df
|
||||||
|
|
||||||
|
def _apply_sort(self, df):
|
||||||
|
if df is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
sorted_columns = []
|
||||||
|
sorted_asc = []
|
||||||
|
for sort_def in self._state.sorted:
|
||||||
|
if sort_def.direction != 0:
|
||||||
|
sorted_columns.append(sort_def.column_id)
|
||||||
|
asc = sort_def.direction == 1
|
||||||
|
sorted_asc.append(asc)
|
||||||
|
|
||||||
|
if sorted_columns:
|
||||||
|
df = df.sort_values(by=sorted_columns, ascending=sorted_asc)
|
||||||
|
|
||||||
|
return df
|
||||||
|
|
||||||
|
def _apply_filter(self, df):
|
||||||
|
if df is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
for col_id, values in self._state.filtered.items():
|
||||||
|
if col_id == FILTER_INPUT_CID and values is not None:
|
||||||
|
if self._datagrid_filter.get_query_type() == DG_QUERY_FILTER:
|
||||||
|
visible_columns = [c.col_id for c in self._state.columns if c.visible and c.col_id in df.columns]
|
||||||
|
df = df[df[visible_columns].map(lambda x: values.lower() in str(x).lower()).any(axis=1)]
|
||||||
|
else:
|
||||||
|
pass # we return all the row (but we will keep the highlight)
|
||||||
|
|
||||||
|
else:
|
||||||
|
df = df[df[col_id].astype(str).isin(values)]
|
||||||
|
return df
|
||||||
|
|
||||||
|
def _get_filtered_df(self):
|
||||||
|
if self._df is None:
|
||||||
|
return DataFrame()
|
||||||
|
|
||||||
|
df = self._df.copy()
|
||||||
|
df = self._apply_sort(df) # need to keep the real type to sort
|
||||||
|
df = self._apply_filter(df)
|
||||||
|
|
||||||
|
return df
|
||||||
|
|
||||||
def init_from_dataframe(self, df, init_state=True):
|
def init_from_dataframe(self, df, init_state=True):
|
||||||
|
|
||||||
def _get_column_type(dtype):
|
def _get_column_type(dtype):
|
||||||
@@ -214,6 +268,11 @@ class DataGrid(MultipleInstance):
|
|||||||
|
|
||||||
self._state.save()
|
self._state.save()
|
||||||
|
|
||||||
|
def filter(self):
|
||||||
|
logger.debug("filter")
|
||||||
|
self._state.filtered[FILTER_INPUT_CID] = self._datagrid_filter.get_query()
|
||||||
|
return self.mk_body_container(redraw_scrollbars=True)
|
||||||
|
|
||||||
def mk_headers(self):
|
def mk_headers(self):
|
||||||
resize_cmd = self.commands.set_column_width()
|
resize_cmd = self.commands.set_column_width()
|
||||||
move_cmd = self.commands.move_column()
|
move_cmd = self.commands.move_column()
|
||||||
@@ -268,9 +327,10 @@ class DataGrid(MultipleInstance):
|
|||||||
res = []
|
res = []
|
||||||
if index > 0:
|
if index > 0:
|
||||||
res.append(Span(value_str[:index], cls=f"{css_class}"))
|
res.append(Span(value_str[:index], cls=f"{css_class}"))
|
||||||
res.append(Span(value_str[index:index + len_keyword], cls=f"{css_class} dt2-highlight-1"))
|
res.append(Span(value_str[index:index + len_keyword], cls=f"dt2-highlight-1"))
|
||||||
if index + len_keyword < len(value_str):
|
if index + len_keyword < len(value_str):
|
||||||
res.append(Span(value_str[index + len_keyword:], cls=f"{css_class}"))
|
res.append(Span(value_str[index + len_keyword:], cls=f"{css_class}"))
|
||||||
|
|
||||||
return Span(*res, cls=f"{css_class} truncate") if len(res) > 1 else res[0]
|
return Span(*res, cls=f"{css_class} truncate") if len(res) > 1 else res[0]
|
||||||
|
|
||||||
column_type = col_def.type
|
column_type = col_def.type
|
||||||
@@ -322,7 +382,7 @@ class DataGrid(MultipleInstance):
|
|||||||
OPTIMIZED: Extract filter keyword once instead of 10,000 times.
|
OPTIMIZED: Extract filter keyword once instead of 10,000 times.
|
||||||
OPTIMIZED: Uses OptimizedDiv for rows instead of Div for faster rendering.
|
OPTIMIZED: Uses OptimizedDiv for rows instead of Div for faster rendering.
|
||||||
"""
|
"""
|
||||||
df = self._df # self._get_filtered_df()
|
df = self._get_filtered_df()
|
||||||
start = page_index * DATAGRID_PAGE_SIZE
|
start = page_index * DATAGRID_PAGE_SIZE
|
||||||
end = start + DATAGRID_PAGE_SIZE
|
end = start + DATAGRID_PAGE_SIZE
|
||||||
if self._state.ns_total_rows > end:
|
if self._state.ns_total_rows > end:
|
||||||
@@ -344,6 +404,14 @@ class DataGrid(MultipleInstance):
|
|||||||
|
|
||||||
return rows
|
return rows
|
||||||
|
|
||||||
|
def mk_body_container(self, redraw_scrollbars=False):
|
||||||
|
return Div(
|
||||||
|
self.mk_body(),
|
||||||
|
Script(f"initDataGridScrollbars('{self._id}');") if redraw_scrollbars else None,
|
||||||
|
cls="dt2-body-container",
|
||||||
|
id=f"tb_{self._id}"
|
||||||
|
)
|
||||||
|
|
||||||
def mk_body(self):
|
def mk_body(self):
|
||||||
return Div(
|
return Div(
|
||||||
*self.mk_body_content_page(0),
|
*self.mk_body_content_page(0),
|
||||||
@@ -372,12 +440,9 @@ class DataGrid(MultipleInstance):
|
|||||||
self.mk_headers(),
|
self.mk_headers(),
|
||||||
cls="dt2-header-container"
|
cls="dt2-header-container"
|
||||||
),
|
),
|
||||||
# Body container - scroll via JS, scrollbars hidden
|
|
||||||
Div(
|
self.mk_body_container(), # Body container - scroll via JS, scrollbars hidden
|
||||||
self.mk_body(),
|
|
||||||
cls="dt2-body-container",
|
|
||||||
id=f"tb_{self._id}"
|
|
||||||
),
|
|
||||||
# Footer container - no scroll
|
# Footer container - no scroll
|
||||||
Div(
|
Div(
|
||||||
self.mk_footers(),
|
self.mk_footers(),
|
||||||
@@ -470,13 +535,13 @@ class DataGrid(MultipleInstance):
|
|||||||
if self._state.ne_df is None:
|
if self._state.ne_df is None:
|
||||||
return Div("No data to display !")
|
return Div("No data to display !")
|
||||||
|
|
||||||
from myfasthtml.controls.DataGridFilter import DataGridFilter
|
|
||||||
return Div(
|
return Div(
|
||||||
Div(DataGridFilter(self), cls="mb-2"),
|
Div(self._datagrid_filter, cls="mb-2"),
|
||||||
self.mk_table(),
|
self.mk_table(),
|
||||||
Script(f"initDataGrid('{self._id}');"),
|
Script(f"initDataGrid('{self._id}');"),
|
||||||
id=self._id,
|
id=self._id,
|
||||||
style="height: 100%;"
|
cls="grid",
|
||||||
|
style="height: 100%; grid-template-rows: auto 1fr;"
|
||||||
)
|
)
|
||||||
|
|
||||||
def __ft__(self):
|
def __ft__(self):
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
import logging
|
|
||||||
|
|
||||||
from fasthtml.components import *
|
|
||||||
|
|
||||||
from myfasthtml.controls.BaseCommands import BaseCommands
|
|
||||||
from myfasthtml.controls.helpers import mk
|
|
||||||
from myfasthtml.core.commands import Command
|
|
||||||
from myfasthtml.core.dbmanager import DbObject
|
|
||||||
from myfasthtml.core.instances import MultipleInstance
|
|
||||||
from myfasthtml.icons.fluent import brain_circuit20_regular
|
|
||||||
from myfasthtml.icons.fluent_p1 import filter20_regular, search20_regular
|
|
||||||
from myfasthtml.icons.fluent_p2 import dismiss_circle20_regular
|
|
||||||
|
|
||||||
logger = logging.getLogger("DataGridFilter")
|
|
||||||
|
|
||||||
filter_type = {
|
|
||||||
"filter": filter20_regular,
|
|
||||||
"search": search20_regular,
|
|
||||||
"ai": brain_circuit20_regular
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class DataGridFilterState(DbObject):
|
|
||||||
def __init__(self, owner):
|
|
||||||
with self.initializing():
|
|
||||||
super().__init__(owner)
|
|
||||||
self.filter_type: str = "filter"
|
|
||||||
|
|
||||||
|
|
||||||
class Commands(BaseCommands):
|
|
||||||
def change_filter_type(self):
|
|
||||||
return Command("ChangeFilterType",
|
|
||||||
"Change filter type",
|
|
||||||
self._owner,
|
|
||||||
self._owner.change_filter_type).htmx(target=f"#{self._id}")
|
|
||||||
|
|
||||||
def on_filter_changed(self):
|
|
||||||
return Command("FilterChanged",
|
|
||||||
"Filter changed",
|
|
||||||
self._owner,
|
|
||||||
self._owner.filter_changed).htmx(target=None)
|
|
||||||
|
|
||||||
|
|
||||||
class DataGridFilter(MultipleInstance):
|
|
||||||
def __init__(self, parent, _id=None):
|
|
||||||
super().__init__(parent, _id=_id or "-filter")
|
|
||||||
self.commands = Commands(self)
|
|
||||||
self._state = DataGridFilterState(self)
|
|
||||||
|
|
||||||
def change_filter_type(self):
|
|
||||||
keys = list(filter_type.keys()) # ["filter", "search", "ai"]
|
|
||||||
current_idx = keys.index(self._state.filter_type)
|
|
||||||
self._state.filter_type = keys[(current_idx + 1) % len(keys)]
|
|
||||||
return self
|
|
||||||
|
|
||||||
def filter_changed(self, f):
|
|
||||||
logger.debug(f"filter_changed {f=}")
|
|
||||||
return self
|
|
||||||
|
|
||||||
def render(self):
|
|
||||||
return Div(
|
|
||||||
mk.label(
|
|
||||||
Input(name="f",
|
|
||||||
placeholder="Search...",
|
|
||||||
**self.commands.on_filter_changed().get_htmx_params(escaped=True)),
|
|
||||||
icon=mk.icon(filter_type[self._state.filter_type], command=self.commands.change_filter_type()),
|
|
||||||
cls="input input-sm flex gap-2"
|
|
||||||
),
|
|
||||||
mk.icon(dismiss_circle20_regular, size=24),
|
|
||||||
# Keyboard(self, _id="-keyboard").add("enter", self.commands.on_filter_changed()),
|
|
||||||
cls="flex",
|
|
||||||
id=self._id
|
|
||||||
)
|
|
||||||
|
|
||||||
def __ft__(self):
|
|
||||||
return self.render()
|
|
||||||
97
src/myfasthtml/controls/DataGridQuery.py
Normal file
97
src/myfasthtml/controls/DataGridQuery.py
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import logging
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from fasthtml.components import *
|
||||||
|
|
||||||
|
from myfasthtml.controls.BaseCommands import BaseCommands
|
||||||
|
from myfasthtml.controls.helpers import mk
|
||||||
|
from myfasthtml.core.commands import Command
|
||||||
|
from myfasthtml.core.dbmanager import DbObject
|
||||||
|
from myfasthtml.core.instances import MultipleInstance
|
||||||
|
from myfasthtml.icons.fluent import brain_circuit20_regular
|
||||||
|
from myfasthtml.icons.fluent_p1 import filter20_regular, search20_regular
|
||||||
|
from myfasthtml.icons.fluent_p2 import dismiss_circle20_regular
|
||||||
|
|
||||||
|
logger = logging.getLogger("DataGridFilter")
|
||||||
|
|
||||||
|
DG_QUERY_FILTER = "filter"
|
||||||
|
DG_QUERY_SEARCH = "search"
|
||||||
|
DG_QUERY_AI = "ai"
|
||||||
|
|
||||||
|
query_type = {
|
||||||
|
DG_QUERY_FILTER: filter20_regular,
|
||||||
|
DG_QUERY_SEARCH: search20_regular,
|
||||||
|
DG_QUERY_AI: brain_circuit20_regular
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class DataGridFilterState(DbObject):
|
||||||
|
def __init__(self, owner):
|
||||||
|
with self.initializing():
|
||||||
|
super().__init__(owner)
|
||||||
|
self.filter_type: str = "filter"
|
||||||
|
self.query: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class Commands(BaseCommands):
|
||||||
|
def change_filter_type(self):
|
||||||
|
return Command("ChangeFilterType",
|
||||||
|
"Change filter type",
|
||||||
|
self._owner,
|
||||||
|
self._owner.change_query_type).htmx(target=f"#{self._id}")
|
||||||
|
|
||||||
|
def on_filter_changed(self):
|
||||||
|
return Command("QueryChanged",
|
||||||
|
"Query changed",
|
||||||
|
self._owner,
|
||||||
|
self._owner.query_changed).htmx(target=None)
|
||||||
|
|
||||||
|
def on_cancel_query(self):
|
||||||
|
return Command("CancelQuery",
|
||||||
|
"Cancel query",
|
||||||
|
self._owner,
|
||||||
|
self._owner.query_changed,
|
||||||
|
kwargs={"query": ""}
|
||||||
|
).htmx(target=f"#{self._id}")
|
||||||
|
|
||||||
|
|
||||||
|
class DataGridQuery(MultipleInstance):
|
||||||
|
def __init__(self, parent, _id=None):
|
||||||
|
super().__init__(parent, _id=_id or "-query")
|
||||||
|
self.commands = Commands(self)
|
||||||
|
self._state = DataGridFilterState(self)
|
||||||
|
|
||||||
|
def get_query(self):
|
||||||
|
return self._state.query
|
||||||
|
|
||||||
|
def get_query_type(self):
|
||||||
|
return self._state.filter_type
|
||||||
|
|
||||||
|
def change_query_type(self):
|
||||||
|
keys = list(query_type.keys()) # ["filter", "search", "ai"]
|
||||||
|
current_idx = keys.index(self._state.filter_type)
|
||||||
|
self._state.filter_type = keys[(current_idx + 1) % len(keys)]
|
||||||
|
return self
|
||||||
|
|
||||||
|
def query_changed(self, query):
|
||||||
|
logger.debug(f"query_changed {query=}")
|
||||||
|
self._state.query = query
|
||||||
|
return self
|
||||||
|
|
||||||
|
def render(self):
|
||||||
|
return Div(
|
||||||
|
mk.label(
|
||||||
|
Input(name="query",
|
||||||
|
value=self._state.query if self._state.query is not None else "",
|
||||||
|
placeholder="Search...",
|
||||||
|
**self.commands.on_filter_changed().get_htmx_params(values_encode="json")),
|
||||||
|
icon=mk.icon(query_type[self._state.filter_type], command=self.commands.change_filter_type()),
|
||||||
|
cls="input input-xs flex gap-3"
|
||||||
|
),
|
||||||
|
mk.icon(dismiss_circle20_regular, size=24, command=self.commands.on_cancel_query()),
|
||||||
|
cls="flex",
|
||||||
|
id=self._id
|
||||||
|
)
|
||||||
|
|
||||||
|
def __ft__(self):
|
||||||
|
return self.render()
|
||||||
@@ -14,6 +14,7 @@ logger = logging.getLogger("Commands")
|
|||||||
|
|
||||||
AUTO_SWAP_OOB = "__auto_swap_oob__"
|
AUTO_SWAP_OOB = "__auto_swap_oob__"
|
||||||
|
|
||||||
|
|
||||||
class Command:
|
class Command:
|
||||||
"""
|
"""
|
||||||
Represents the base command class for defining executable actions.
|
Represents the base command class for defining executable actions.
|
||||||
@@ -99,7 +100,7 @@ class Command:
|
|||||||
def get_key(self):
|
def get_key(self):
|
||||||
return self._key
|
return self._key
|
||||||
|
|
||||||
def get_htmx_params(self, escaped=False):
|
def get_htmx_params(self, escaped=False, values_encode=None):
|
||||||
res = {
|
res = {
|
||||||
"hx-post": f"{ROUTE_ROOT}{Routes.Commands}",
|
"hx-post": f"{ROUTE_ROOT}{Routes.Commands}",
|
||||||
"hx-swap": "outerHTML",
|
"hx-swap": "outerHTML",
|
||||||
@@ -120,6 +121,9 @@ class Command:
|
|||||||
if escaped:
|
if escaped:
|
||||||
res["hx-vals"] = html.escape(json.dumps(res["hx-vals"]))
|
res["hx-vals"] = html.escape(json.dumps(res["hx-vals"]))
|
||||||
|
|
||||||
|
if values_encode is "json":
|
||||||
|
res["hx-vals"] = json.dumps(res["hx-vals"])
|
||||||
|
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def execute(self, client_response: dict = None):
|
def execute(self, client_response: dict = None):
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ by generating HTML strings directly instead of creating full FastHTML objects.
|
|||||||
|
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
|
|
||||||
|
from fastcore.xml import FT
|
||||||
from fasthtml.common import NotStr
|
from fasthtml.common import NotStr
|
||||||
|
from fasthtml.components import Span
|
||||||
|
|
||||||
from myfasthtml.core.constants import NO_DEFAULT_VALUE
|
from myfasthtml.core.constants import NO_DEFAULT_VALUE
|
||||||
|
|
||||||
@@ -46,6 +48,8 @@ class OptimizedFt:
|
|||||||
return item.to_html()
|
return item.to_html()
|
||||||
elif isinstance(item, NotStr):
|
elif isinstance(item, NotStr):
|
||||||
return str(item)
|
return str(item)
|
||||||
|
elif isinstance(item, FT):
|
||||||
|
return str(item)
|
||||||
else:
|
else:
|
||||||
raise Exception(f"Unsupported type: {type(item)}, {item=}")
|
raise Exception(f"Unsupported type: {type(item)}, {item=}")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user