I can move columns
This commit is contained in:
@@ -1,5 +1,8 @@
|
|||||||
:root {
|
:root {
|
||||||
--color-border-primary: color-mix(in oklab, var(--color-primary) 40%, #0000);
|
--color-border-primary: color-mix(in oklab, var(--color-primary) 40%, #0000);
|
||||||
|
--color-border: color-mix(in oklab, var(--color-base-content) 20%, #0000);
|
||||||
|
--color-resize: color-mix(in oklab, var(--color-base-content) 50%, #0000);
|
||||||
|
--datagrid-resize-zindex: 1;
|
||||||
--font-sans: ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
--font-sans: ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
||||||
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
|
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
|
||||||
--spacing: 0.25rem;
|
--spacing: 0.25rem;
|
||||||
@@ -854,8 +857,6 @@
|
|||||||
|
|
||||||
/* Cell */
|
/* Cell */
|
||||||
.dt2-cell {
|
.dt2-cell {
|
||||||
--color-border: color-mix(in oklab, var(--color-base-content) 20%, #0000);
|
|
||||||
--color-resize: color-mix(in oklab, var(--color-base-content) 50%, #0000);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
@@ -1053,3 +1054,17 @@
|
|||||||
.dt2-scrollbars-horizontal.dt2-dragging {
|
.dt2-scrollbars-horizontal.dt2-dragging {
|
||||||
background-color: color-mix(in oklab, var(--color-base-content) 30%, #0000);
|
background-color: color-mix(in oklab, var(--color-base-content) 30%, #0000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* *********************************************** */
|
||||||
|
/* ******** DataGrid Column Drag & Drop ********** */
|
||||||
|
/* *********************************************** */
|
||||||
|
|
||||||
|
/* Column being dragged - visual feedback */
|
||||||
|
.dt2-dragging {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Column animation during swap */
|
||||||
|
.dt2-moving {
|
||||||
|
transition: transform 300ms ease;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1495,7 +1495,8 @@ function updateTabs(controllerId) {
|
|||||||
|
|
||||||
function initDataGrid(gridId) {
|
function initDataGrid(gridId) {
|
||||||
initDataGridScrollbars(gridId);
|
initDataGridScrollbars(gridId);
|
||||||
makeDatagridColumnsResizable(gridId)
|
makeDatagridColumnsResizable(gridId);
|
||||||
|
makeDatagridColumnsMovable(gridId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1855,3 +1856,162 @@ function makeDatagridColumnsResizable(datagridId) {
|
|||||||
table.dispatchEvent(resetEvent);
|
table.dispatchEvent(resetEvent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable column reordering via drag and drop on a DataGrid.
|
||||||
|
* Columns can be dragged to new positions with animated transitions.
|
||||||
|
* @param {string} gridId - The DataGrid instance ID
|
||||||
|
*/
|
||||||
|
function makeDatagridColumnsMovable(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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move a column to a new position with animation.
|
||||||
|
* All columns between source and target shift to fill the gap.
|
||||||
|
* @param {HTMLElement} table - The table element
|
||||||
|
* @param {string} sourceColId - Column ID to move
|
||||||
|
* @param {string} targetColId - Column ID to move next to
|
||||||
|
*/
|
||||||
|
function moveColumn(table, sourceColId, targetColId) {
|
||||||
|
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}"]`);
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
if (sourceIdx === targetIdx) return;
|
||||||
|
|
||||||
|
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];
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
// 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 = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -94,6 +94,13 @@ class Commands(BaseCommands):
|
|||||||
self._owner.set_column_width
|
self._owner.set_column_width
|
||||||
).htmx(target=None)
|
).htmx(target=None)
|
||||||
|
|
||||||
|
def move_column(self):
|
||||||
|
return Command("MoveColumn",
|
||||||
|
"Move column to new position",
|
||||||
|
self._owner,
|
||||||
|
self._owner.move_column
|
||||||
|
).htmx(target=None)
|
||||||
|
|
||||||
|
|
||||||
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):
|
||||||
@@ -176,8 +183,39 @@ class DataGrid(MultipleInstance):
|
|||||||
|
|
||||||
self._state.save()
|
self._state.save()
|
||||||
|
|
||||||
|
def move_column(self, source_col_id: str, target_col_id: str):
|
||||||
|
"""Move column to new position. Called via Command from JS."""
|
||||||
|
logger.debug(f"move_column: {source_col_id=} {target_col_id=}")
|
||||||
|
|
||||||
|
# Find indices
|
||||||
|
source_idx = None
|
||||||
|
target_idx = None
|
||||||
|
for i, col in enumerate(self._state.columns):
|
||||||
|
if col.col_id == source_col_id:
|
||||||
|
source_idx = i
|
||||||
|
if col.col_id == target_col_id:
|
||||||
|
target_idx = i
|
||||||
|
|
||||||
|
if source_idx is None or target_idx is None:
|
||||||
|
logger.warning(f"move_column: column not found {source_col_id=} {target_col_id=}")
|
||||||
|
return
|
||||||
|
|
||||||
|
if source_idx == target_idx:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Remove source column and insert at target position
|
||||||
|
col = self._state.columns.pop(source_idx)
|
||||||
|
# Adjust target index if source was before target
|
||||||
|
if source_idx < target_idx:
|
||||||
|
self._state.columns.insert(target_idx, col)
|
||||||
|
else:
|
||||||
|
self._state.columns.insert(target_idx, col)
|
||||||
|
|
||||||
|
self._state.save()
|
||||||
|
|
||||||
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()
|
||||||
|
|
||||||
def _mk_header_name(col_def: DataGridColumnState):
|
def _mk_header_name(col_def: DataGridColumnState):
|
||||||
return Div(
|
return Div(
|
||||||
@@ -199,7 +237,8 @@ class DataGrid(MultipleInstance):
|
|||||||
return Div(
|
return Div(
|
||||||
*[_mk_header(col_def) for col_def in self._state.columns],
|
*[_mk_header(col_def) for col_def in self._state.columns],
|
||||||
cls=header_class,
|
cls=header_class,
|
||||||
id=f"th_{self._id}"
|
id=f"th_{self._id}",
|
||||||
|
data_move_command_id=move_cmd.id
|
||||||
)
|
)
|
||||||
|
|
||||||
def mk_body_cell_content(self, col_pos, row_index, col_def: DataGridColumnState, filter_keyword_lower=None):
|
def mk_body_cell_content(self, col_pos, row_index, col_def: DataGridColumnState, filter_keyword_lower=None):
|
||||||
|
|||||||
Reference in New Issue
Block a user