I can select range with visual feedback
This commit is contained in:
@@ -671,12 +671,8 @@
|
||||
const element = document.getElementById(candidate.elementId);
|
||||
const dynamicValues = func(event, element, candidate.node.combinationStr);
|
||||
if (dynamicValues && typeof dynamicValues === 'object') {
|
||||
// Suffix each key with _mousedown
|
||||
const suffixed = {};
|
||||
for (const [key, value] of Object.entries(dynamicValues)) {
|
||||
suffixed[key + '_mousedown'] = value;
|
||||
}
|
||||
mousedownJsValues[candidate.elementId] = suffixed;
|
||||
// Store raw values - _mousedown suffix added at mouseup time
|
||||
mousedownJsValues[candidate.elementId] = dynamicValues;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -699,12 +695,39 @@
|
||||
// Clear pending state
|
||||
MouseRegistry.mousedownPending = null;
|
||||
|
||||
// Remove mousemove listener (no longer needed)
|
||||
// Remove mousemove listener (threshold detection no longer needed)
|
||||
if (MouseRegistry.mousemoveHandler) {
|
||||
document.removeEventListener('mousemove', MouseRegistry.mousemoveHandler);
|
||||
MouseRegistry.mousemoveHandler = null;
|
||||
}
|
||||
|
||||
// Attach on_move handler if any candidate has 'on-move' config
|
||||
const onMoveCandidates = MouseRegistry.mousedownState.candidates.filter(
|
||||
c => c.node.config && c.node.config['on-move']
|
||||
);
|
||||
if (onMoveCandidates.length > 0) {
|
||||
let rafId = null;
|
||||
MouseRegistry.mousemoveHandler = (moveEvent) => {
|
||||
if (rafId) return;
|
||||
rafId = requestAnimationFrame(() => {
|
||||
for (const candidate of onMoveCandidates) {
|
||||
const funcName = candidate.node.config['on-move'];
|
||||
try {
|
||||
const func = window[funcName];
|
||||
if (typeof func === 'function') {
|
||||
const mousedownValues = mousedownJsValues[candidate.elementId] || null;
|
||||
func(moveEvent, candidate.node.combinationStr, mousedownValues);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error calling on_move function:', e);
|
||||
}
|
||||
}
|
||||
rafId = null;
|
||||
});
|
||||
};
|
||||
document.addEventListener('mousemove', MouseRegistry.mousemoveHandler);
|
||||
}
|
||||
|
||||
// For right button: prevent context menu during drag
|
||||
if (pendingData.button === 2) {
|
||||
const preventContextMenu = (e) => {
|
||||
@@ -907,9 +930,11 @@
|
||||
Object.assign(values, config['hx-vals']);
|
||||
}
|
||||
|
||||
// 2. Merge mousedown JS values (already suffixed with _mousedown)
|
||||
// 2. Merge mousedown JS values with _mousedown suffix
|
||||
if (match.mousedownJsValues) {
|
||||
Object.assign(values, match.mousedownJsValues);
|
||||
for (const [key, value] of Object.entries(match.mousedownJsValues)) {
|
||||
values[key + '_mousedown'] = value;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Call JS function at mouseup time, suffix with _mouseup
|
||||
|
||||
@@ -186,8 +186,10 @@ function bindTooltipsWithDelegation(elementId) {
|
||||
|
||||
// Add a single mouseenter and mouseleave listener to the parent element
|
||||
element.addEventListener("mouseenter", (event) => {
|
||||
// Early exit - check mf-no-tooltip FIRST (before any DOM work)
|
||||
if (element.hasAttribute("mf-no-tooltip")) {
|
||||
const target = event.target;
|
||||
|
||||
// Early exit - check mf-no-tooltip on the registered element OR any ancestor of the target
|
||||
if (element.hasAttribute("mf-no-tooltip") || target.closest("[mf-no-tooltip]")) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -196,7 +198,7 @@ function bindTooltipsWithDelegation(elementId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cell = event.target.closest("[data-tooltip]");
|
||||
const cell = target.closest("[data-tooltip]");
|
||||
if (!cell) {
|
||||
return;
|
||||
}
|
||||
@@ -207,7 +209,7 @@ function bindTooltipsWithDelegation(elementId) {
|
||||
tooltipRafScheduled = false;
|
||||
|
||||
// Check again in case tooltip was disabled during RAF delay
|
||||
if (element.hasAttribute("mf-no-tooltip")) {
|
||||
if (element.hasAttribute("mf-no-tooltip") || target.closest("[mf-no-tooltip]")) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -126,6 +126,16 @@
|
||||
background-color: var(--color-selection);
|
||||
}
|
||||
|
||||
.dt2-drag-preview {
|
||||
background-color: var(--color-selection);
|
||||
}
|
||||
|
||||
/* Selection border - outlines the entire selection rectangle */
|
||||
.dt2-selection-border-top { border-top: 2px solid var(--color-primary); }
|
||||
.dt2-selection-border-bottom { border-bottom: 2px solid var(--color-primary); }
|
||||
.dt2-selection-border-left { border-left: 2px solid var(--color-primary); }
|
||||
.dt2-selection-border-right { border-right: 2px solid var(--color-primary); }
|
||||
|
||||
|
||||
/* *********************************************** */
|
||||
/* ******** DataGrid Fixed Header/Footer ******** */
|
||||
|
||||
@@ -633,10 +633,17 @@ function updateDatagridSelection(datagridId) {
|
||||
const selectionManager = document.getElementById(`tsm_${datagridId}`);
|
||||
if (!selectionManager) return;
|
||||
|
||||
// Clear previous selections
|
||||
document.querySelectorAll('.dt2-selected-focus, .dt2-selected-cell, .dt2-selected-row, .dt2-selected-column').forEach((element) => {
|
||||
element.classList.remove('dt2-selected-focus', 'dt2-selected-cell', 'dt2-selected-row', 'dt2-selected-column');
|
||||
element.style.userSelect = 'none';
|
||||
// Re-enable tooltips after drag
|
||||
const wrapper = document.getElementById(`tw_${datagridId}`);
|
||||
if (wrapper) wrapper.removeAttribute('mf-no-tooltip');
|
||||
|
||||
// Clear browser text selection to prevent stale ranges from reappearing
|
||||
window.getSelection()?.removeAllRanges();
|
||||
|
||||
// Clear previous selections and drag preview
|
||||
document.querySelectorAll('.dt2-selected-focus, .dt2-selected-cell, .dt2-selected-row, .dt2-selected-column, .dt2-drag-preview, .dt2-selection-border-top, .dt2-selection-border-bottom, .dt2-selection-border-left, .dt2-selection-border-right').forEach((element) => {
|
||||
element.classList.remove('dt2-selected-focus', 'dt2-selected-cell', 'dt2-selected-row', 'dt2-selected-column', 'dt2-drag-preview', 'dt2-selection-border-top', 'dt2-selection-border-bottom', 'dt2-selection-border-left', 'dt2-selection-border-right');
|
||||
element.style.userSelect = '';
|
||||
});
|
||||
|
||||
// Loop through the children of the selection manager
|
||||
@@ -654,7 +661,6 @@ function updateDatagridSelection(datagridId) {
|
||||
const cellElement = document.getElementById(`${elementId}`);
|
||||
if (cellElement) {
|
||||
cellElement.classList.add('dt2-selected-cell');
|
||||
cellElement.style.userSelect = 'text';
|
||||
}
|
||||
} else if (selectionType === 'row') {
|
||||
const rowElement = document.getElementById(`${elementId}`);
|
||||
@@ -687,7 +693,10 @@ function updateDatagridSelection(datagridId) {
|
||||
const cell = document.getElementById(cellId);
|
||||
if (cell) {
|
||||
cell.classList.add('dt2-selected-cell');
|
||||
cell.style.userSelect = 'text';
|
||||
if (row === minRowNum) cell.classList.add('dt2-selection-border-top');
|
||||
if (row === maxRowNum) cell.classList.add('dt2-selection-border-bottom');
|
||||
if (col === minColNum) cell.classList.add('dt2-selection-border-left');
|
||||
if (col === maxColNum) cell.classList.add('dt2-selection-border-right');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -710,4 +719,72 @@ function getCellId(event) {
|
||||
return {cell_id: cell.id};
|
||||
}
|
||||
return {cell_id: null};
|
||||
}
|
||||
|
||||
/**
|
||||
* Highlight the drag selection range in real time during a mousedown>mouseup drag.
|
||||
* Called by mouse.js on each animation frame while dragging.
|
||||
* Applies .dt2-drag-preview to all cells in the rectangle between the start and
|
||||
* current cell. The preview is cleared by updateDatagridSelection() when the server
|
||||
* responds with the final selection.
|
||||
*
|
||||
* @param {MouseEvent} event - The current mousemove event
|
||||
* @param {string} combination - The active mouse combination (e.g. "mousedown>mouseup")
|
||||
* @param {Object|null} mousedownResult - Result of getCellId() at mousedown, or null
|
||||
*/
|
||||
function highlightDatagridDragRange(event, combination, mousedownResult) {
|
||||
if (!mousedownResult || !mousedownResult.cell_id) return;
|
||||
|
||||
const currentCell = event.target.closest('.dt2-cell');
|
||||
if (!currentCell || !currentCell.id) return;
|
||||
|
||||
const startCellId = mousedownResult.cell_id;
|
||||
const endCellId = currentCell.id;
|
||||
|
||||
// Find the table from the start cell to scope the query
|
||||
const startCell = document.getElementById(startCellId);
|
||||
if (!startCell) return;
|
||||
const table = startCell.closest('.dt2-table');
|
||||
if (!table) return;
|
||||
|
||||
// Extract grid ID from table id: "t_{gridId}" -> "{gridId}"
|
||||
const gridId = table.id.substring(2);
|
||||
|
||||
// Disable tooltips during drag
|
||||
const wrapper = document.getElementById(`tw_${gridId}`);
|
||||
if (wrapper) wrapper.setAttribute('mf-no-tooltip', '');
|
||||
|
||||
// Parse col/row by splitting on "-" and taking the last two numeric parts
|
||||
const startParts = startCellId.split('-');
|
||||
const startCol = parseInt(startParts[startParts.length - 2]);
|
||||
const startRow = parseInt(startParts[startParts.length - 1]);
|
||||
|
||||
const endParts = endCellId.split('-');
|
||||
const endCol = parseInt(endParts[endParts.length - 2]);
|
||||
const endRow = parseInt(endParts[endParts.length - 1]);
|
||||
|
||||
if (isNaN(startCol) || isNaN(startRow) || isNaN(endCol) || isNaN(endRow)) return;
|
||||
|
||||
// Clear previous selection and drag preview within this table
|
||||
table.querySelectorAll('.dt2-drag-preview, .dt2-selected-cell, .dt2-selected-row, .dt2-selected-column, .dt2-selected-focus, .dt2-selection-border-top, .dt2-selection-border-bottom, .dt2-selection-border-left, .dt2-selection-border-right')
|
||||
.forEach(c => c.classList.remove('dt2-drag-preview', 'dt2-selected-cell', 'dt2-selected-row', 'dt2-selected-column', 'dt2-selected-focus', 'dt2-selection-border-top', 'dt2-selection-border-bottom', 'dt2-selection-border-left', 'dt2-selection-border-right'));
|
||||
|
||||
// Apply preview to all cells in the rectangular range
|
||||
const minCol = Math.min(startCol, endCol);
|
||||
const maxCol = Math.max(startCol, endCol);
|
||||
const minRow = Math.min(startRow, endRow);
|
||||
const maxRow = Math.max(startRow, endRow);
|
||||
|
||||
for (let col = minCol; col <= maxCol; col++) {
|
||||
for (let row = minRow; row <= maxRow; row++) {
|
||||
const cell = document.getElementById(`tcell_${gridId}-${col}-${row}`);
|
||||
if (cell) {
|
||||
cell.classList.add('dt2-drag-preview');
|
||||
if (row === minRow) cell.classList.add('dt2-selection-border-top');
|
||||
if (row === maxRow) cell.classList.add('dt2-selection-border-bottom');
|
||||
if (col === minCol) cell.classList.add('dt2-selection-border-left');
|
||||
if (col === maxCol) cell.classList.add('dt2-selection-border-right');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -238,7 +238,9 @@ class DataGrid(MultipleInstance):
|
||||
|
||||
# other definitions
|
||||
self._mouse_support = {
|
||||
"mousedown>mouseup": {"command": self.commands.on_mouse_selection(), "hx_vals": "js:getCellId()"},
|
||||
"mousedown>mouseup": {"command": self.commands.on_mouse_selection(),
|
||||
"hx_vals": "js:getCellId()",
|
||||
"on_move": "js:highlightDatagridDragRange()"},
|
||||
"click": {"command": self.commands.on_click(), "hx_vals": "js:getCellId()"},
|
||||
"ctrl+click": {"command": self.commands.on_click(), "hx_vals": "js:getCellId()"},
|
||||
"shift+click": {"command": self.commands.on_click(), "hx_vals": "js:getCellId()"},
|
||||
@@ -500,6 +502,9 @@ class DataGrid(MultipleInstance):
|
||||
if (is_inside and
|
||||
cell_id_mousedown and cell_id_mouseup and
|
||||
cell_id_mousedown.startswith("tcell_") and cell_id_mouseup.startswith("tcell_")):
|
||||
|
||||
self._update_current_position(None)
|
||||
|
||||
pos_mouse_down = self._get_pos_from_element_id(cell_id_mousedown)
|
||||
pos_mouse_up = self._get_pos_from_element_id(cell_id_mouseup)
|
||||
|
||||
|
||||
@@ -78,7 +78,8 @@ class Mouse(MultipleInstance):
|
||||
def add(self, sequence: str, command: Command = None, *,
|
||||
hx_post: str = None, hx_get: str = None, hx_put: str = None,
|
||||
hx_delete: str = None, hx_patch: str = None,
|
||||
hx_target: str = None, hx_swap: str = None, hx_vals=None):
|
||||
hx_target: str = None, hx_swap: str = None, hx_vals=None,
|
||||
on_move: str = None):
|
||||
"""
|
||||
Add a mouse combination with optional command and HTMX parameters.
|
||||
|
||||
@@ -99,6 +100,11 @@ class Mouse(MultipleInstance):
|
||||
hx_vals: HTMX values dict or "js:functionName()" for dynamic values.
|
||||
For mousedown>mouseup actions, the JS function is called at both
|
||||
mousedown and mouseup, with results suffixed ``_mousedown`` and ``_mouseup``.
|
||||
on_move: Client-side JS function called on each animation frame during a drag,
|
||||
using ``"js:functionName()"`` format. Only valid with ``mousedown>mouseup``
|
||||
sequences. The function receives ``(event, combination, mousedown_result)``
|
||||
where ``mousedown_result`` is the raw result of ``hx_vals`` at mousedown,
|
||||
or ``None`` if ``hx_vals`` is not set. Return value is ignored.
|
||||
|
||||
Returns:
|
||||
self for method chaining
|
||||
@@ -117,6 +123,7 @@ class Mouse(MultipleInstance):
|
||||
"hx_target": hx_target,
|
||||
"hx_swap": hx_swap,
|
||||
"hx_vals": hx_vals,
|
||||
"on_move": on_move,
|
||||
}
|
||||
return self
|
||||
|
||||
@@ -172,6 +179,15 @@ class Mouse(MultipleInstance):
|
||||
except json.JSONDecodeError as e:
|
||||
raise ValueError(f"hx_vals must be a dict or 'js:functionName()', got invalid JSON: {e}")
|
||||
|
||||
# Handle on_move - client-side function for real-time drag feedback
|
||||
on_move = combination_data.get("on_move")
|
||||
if on_move is not None:
|
||||
if isinstance(on_move, str) and on_move.startswith("js:"):
|
||||
func_name = on_move[3:].rstrip("()")
|
||||
params["on-move"] = func_name
|
||||
else:
|
||||
raise ValueError(f"on_move must be 'js:functionName()', got: {on_move!r}")
|
||||
|
||||
return params
|
||||
|
||||
def render(self):
|
||||
|
||||
Reference in New Issue
Block a user