Added mouse selection
This commit is contained in:
@@ -14,7 +14,15 @@
|
||||
pendingMatches: [], // Array of matches waiting for timeout
|
||||
sequenceTimeout: 500, // 500ms timeout for sequences
|
||||
clickHandler: null,
|
||||
contextmenuHandler: null
|
||||
contextmenuHandler: null,
|
||||
mousedownState: null, // Active drag state (only after movement detected)
|
||||
suppressNextClick: false, // Prevents click from firing after mousedown>mouseup
|
||||
mousedownHandler: null, // Handler reference for cleanup
|
||||
mouseupHandler: null, // Handler reference for cleanup
|
||||
mousedownSafetyTimeout: null, // Safety timeout if mouseup never arrives
|
||||
mousedownPending: null, // Pending mousedown data (before movement detected)
|
||||
mousemoveHandler: null, // Handler for detecting drag movement
|
||||
dragThreshold: 5 // Minimum pixels to consider it a drag (not a click)
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -33,6 +41,58 @@
|
||||
return aliasMap[normalized] || normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an action Set contains a mousedown>mouseup action
|
||||
* @param {Set} actionSet - Set of normalized actions
|
||||
* @returns {boolean} - True if contains mousedown>mouseup or rmousedown>mouseup
|
||||
*/
|
||||
function isMousedownUpAction(actionSet) {
|
||||
return actionSet.has('mousedown>mouseup') || actionSet.has('rmousedown>mouseup');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the expected mouse button for a mousedown>mouseup action
|
||||
* @param {Set} actionSet - Set of normalized actions
|
||||
* @returns {number} - 0 for left button, 2 for right button
|
||||
*/
|
||||
function getMousedownUpButton(actionSet) {
|
||||
return actionSet.has('rmousedown>mouseup') ? 2 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan all registered element trees to find mousedown>mouseup candidates
|
||||
* that are the next expected step given the current snapshot history.
|
||||
* @param {Array} snapshotHistory - Current snapshot history
|
||||
* @returns {Array} - Array of { elementId, node, actionSet, button } candidates
|
||||
*/
|
||||
function checkTreesForMousedownUp(snapshotHistory) {
|
||||
const candidates = [];
|
||||
|
||||
for (const [elementId, data] of MouseRegistry.elements) {
|
||||
const element = document.getElementById(elementId);
|
||||
if (!element) continue;
|
||||
|
||||
// Traverse tree to current position in history
|
||||
const currentNode = traverseTree(data.tree, snapshotHistory);
|
||||
if (!currentNode) continue;
|
||||
|
||||
// Check children for mousedown>mouseup actions
|
||||
for (const [key, childNode] of currentNode.children) {
|
||||
const actionSet = new Set(key.split('+'));
|
||||
if (isMousedownUpAction(actionSet)) {
|
||||
candidates.push({
|
||||
elementId: elementId,
|
||||
node: childNode,
|
||||
actionSet: actionSet,
|
||||
button: getMousedownUpButton(actionSet)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return candidates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a unique string key from a Set of actions for Map indexing
|
||||
* @param {Set} actionSet - Set of normalized actions
|
||||
@@ -226,6 +286,16 @@
|
||||
* @param {MouseEvent} event - The mouse event
|
||||
*/
|
||||
function handleGlobalClick(event) {
|
||||
// Suppress click that fires after mousedown>mouseup drag
|
||||
if (MouseRegistry.suppressNextClick) {
|
||||
MouseRegistry.suppressNextClick = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't process clicks during active drag mode (movement detected)
|
||||
// But DO process clicks if only mousedownPending (no movement = normal click)
|
||||
if (MouseRegistry.mousedownState) return;
|
||||
|
||||
// DEBUG: Measure click handler performance
|
||||
const clickStart = performance.now();
|
||||
const elementCount = MouseRegistry.elements.size;
|
||||
@@ -362,6 +432,10 @@
|
||||
* @param {MouseEvent} event - The mouse event
|
||||
*/
|
||||
function handleElementRightClick(event) {
|
||||
// Don't process right-clicks during active drag with right button
|
||||
// But DO process if only pending (no movement = normal right-click)
|
||||
if (MouseRegistry.mousedownState && MouseRegistry.mousedownState.button === 2) return;
|
||||
|
||||
// Find which registered element was clicked
|
||||
const elementId = findRegisteredElement(event.target);
|
||||
|
||||
@@ -489,6 +563,400 @@
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up mousedown state and safety timeout
|
||||
*/
|
||||
function clearMousedownState() {
|
||||
if (MouseRegistry.mousedownSafetyTimeout) {
|
||||
clearTimeout(MouseRegistry.mousedownSafetyTimeout);
|
||||
MouseRegistry.mousedownSafetyTimeout = null;
|
||||
}
|
||||
|
||||
// Remove mousemove listener if attached
|
||||
if (MouseRegistry.mousemoveHandler) {
|
||||
document.removeEventListener('mousemove', MouseRegistry.mousemoveHandler);
|
||||
MouseRegistry.mousemoveHandler = null;
|
||||
}
|
||||
|
||||
MouseRegistry.mousedownState = null;
|
||||
MouseRegistry.mousedownPending = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle global mousedown events for mousedown>mouseup actions.
|
||||
* Stores pending state and waits for movement to activate drag mode.
|
||||
* @param {MouseEvent} event - The mousedown event
|
||||
*/
|
||||
function handleMouseDown(event) {
|
||||
// Exit early if already in mousedown state or pending
|
||||
if (MouseRegistry.mousedownState || MouseRegistry.mousedownPending) return;
|
||||
|
||||
// Check if any registered tree has a mousedown>mouseup action as the next step
|
||||
const candidates = checkTreesForMousedownUp(MouseRegistry.snapshotHistory);
|
||||
if (candidates.length === 0) return;
|
||||
|
||||
// Filter candidates by button match
|
||||
const matchingCandidates = candidates.filter(c => c.button === event.button);
|
||||
if (matchingCandidates.length === 0) return;
|
||||
|
||||
// Check if mousedown target is inside any registered element
|
||||
const hasFocusMap = {};
|
||||
for (const candidate of matchingCandidates) {
|
||||
const element = document.getElementById(candidate.elementId);
|
||||
if (element) {
|
||||
hasFocusMap[candidate.elementId] = document.activeElement === element;
|
||||
}
|
||||
}
|
||||
|
||||
// Store PENDING mousedown state (not activated yet)
|
||||
MouseRegistry.mousedownPending = {
|
||||
button: event.button,
|
||||
ctrlKey: event.ctrlKey || event.metaKey,
|
||||
shiftKey: event.shiftKey,
|
||||
altKey: event.altKey,
|
||||
startX: event.clientX,
|
||||
startY: event.clientY,
|
||||
mousedownEvent: event,
|
||||
hasFocusMap: hasFocusMap,
|
||||
candidates: matchingCandidates
|
||||
};
|
||||
|
||||
// Add mousemove listener to detect drag
|
||||
MouseRegistry.mousemoveHandler = (moveEvent) => {
|
||||
if (!MouseRegistry.mousedownPending) return;
|
||||
|
||||
const deltaX = Math.abs(moveEvent.clientX - MouseRegistry.mousedownPending.startX);
|
||||
const deltaY = Math.abs(moveEvent.clientY - MouseRegistry.mousedownPending.startY);
|
||||
|
||||
// If moved beyond threshold, activate drag mode
|
||||
if (deltaX > MouseRegistry.dragThreshold || deltaY > MouseRegistry.dragThreshold) {
|
||||
activateDragMode(MouseRegistry.mousedownPending);
|
||||
}
|
||||
};
|
||||
document.addEventListener('mousemove', MouseRegistry.mousemoveHandler);
|
||||
|
||||
// Safety timeout: clean up if mouseup never arrives (5 seconds)
|
||||
MouseRegistry.mousedownSafetyTimeout = setTimeout(() => {
|
||||
clearMousedownState();
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate drag mode after movement detected.
|
||||
* Called by mousemove handler when threshold exceeded.
|
||||
* @param {Object} pendingData - The pending mousedown data
|
||||
*/
|
||||
function activateDragMode(pendingData) {
|
||||
if (!pendingData) return;
|
||||
|
||||
const event = pendingData.mousedownEvent;
|
||||
|
||||
// Suspend sequence timeout (keep pendingMatches but stop the timer)
|
||||
if (MouseRegistry.pendingTimeout) {
|
||||
clearTimeout(MouseRegistry.pendingTimeout);
|
||||
MouseRegistry.pendingTimeout = null;
|
||||
}
|
||||
|
||||
// Call hx-vals-extra JS functions at mousedown time, suffix results with _mousedown
|
||||
const mousedownJsValues = {};
|
||||
for (const candidate of pendingData.candidates) {
|
||||
const config = candidate.node.config;
|
||||
if (!config) continue;
|
||||
|
||||
const extra = config['hx-vals-extra'];
|
||||
if (extra && extra.js) {
|
||||
try {
|
||||
const func = window[extra.js];
|
||||
if (typeof func === 'function') {
|
||||
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;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error calling dynamic hx-vals function at mousedown:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Activate drag state
|
||||
MouseRegistry.mousedownState = {
|
||||
button: pendingData.button,
|
||||
ctrlKey: pendingData.ctrlKey,
|
||||
shiftKey: pendingData.shiftKey,
|
||||
altKey: pendingData.altKey,
|
||||
hasFocusMap: pendingData.hasFocusMap,
|
||||
candidates: pendingData.candidates,
|
||||
mousedownJsValues: mousedownJsValues
|
||||
};
|
||||
|
||||
// Clear pending state
|
||||
MouseRegistry.mousedownPending = null;
|
||||
|
||||
// Remove mousemove listener (no longer needed)
|
||||
if (MouseRegistry.mousemoveHandler) {
|
||||
document.removeEventListener('mousemove', MouseRegistry.mousemoveHandler);
|
||||
MouseRegistry.mousemoveHandler = null;
|
||||
}
|
||||
|
||||
// For right button: prevent context menu during drag
|
||||
if (pendingData.button === 2) {
|
||||
const preventContextMenu = (e) => {
|
||||
e.preventDefault();
|
||||
};
|
||||
document.addEventListener('contextmenu', preventContextMenu, { capture: true, once: true });
|
||||
}
|
||||
|
||||
// Prevent default if on a registered element and not in input context
|
||||
const targetElementId = findRegisteredElement(event.target);
|
||||
if (targetElementId && !isInInputContext()) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle global mouseup events for mousedown>mouseup actions.
|
||||
* Resolves the mousedown>mouseup action when mouseup fires (only if drag was activated).
|
||||
* @param {MouseEvent} event - The mouseup event
|
||||
*/
|
||||
function handleMouseUp(event) {
|
||||
// Case 1: Drag mode was activated (movement detected)
|
||||
if (MouseRegistry.mousedownState) {
|
||||
const mdState = MouseRegistry.mousedownState;
|
||||
|
||||
// Verify button matches mousedown button
|
||||
if (event.button !== mdState.button) return;
|
||||
|
||||
// Build action name based on button
|
||||
const actionName = mdState.button === 2 ? 'rmousedown>mouseup' : 'mousedown>mouseup';
|
||||
|
||||
// Build snapshot Set with modifiers from mousedown time
|
||||
const snapshot = new Set([actionName]);
|
||||
if (mdState.ctrlKey) snapshot.add('ctrl');
|
||||
if (mdState.shiftKey) snapshot.add('shift');
|
||||
if (mdState.altKey) snapshot.add('alt');
|
||||
|
||||
// Add snapshot to history
|
||||
MouseRegistry.snapshotHistory.push(snapshot);
|
||||
|
||||
// Clear mousedown state
|
||||
clearMousedownState();
|
||||
|
||||
// Suppress next click for left button (click fires after mouseup)
|
||||
if (mdState.button === 0) {
|
||||
MouseRegistry.suppressNextClick = true;
|
||||
}
|
||||
|
||||
// Resolve with tree-traversal matching
|
||||
resolveAfterMouseUp(event, mdState);
|
||||
return;
|
||||
}
|
||||
|
||||
// Case 2: Mousedown pending but no movement (it's a click, not a drag)
|
||||
if (MouseRegistry.mousedownPending) {
|
||||
const pending = MouseRegistry.mousedownPending;
|
||||
|
||||
// Verify button matches
|
||||
if (event.button !== pending.button) return;
|
||||
|
||||
// Clean up pending state - let the normal click handler process it
|
||||
clearMousedownState();
|
||||
// Don't suppress click - we want it to fire normally
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve mousedown>mouseup action using tree-traversal matching.
|
||||
* Same logic as handleGlobalClick but uses triggerMousedownUpAction.
|
||||
* @param {MouseEvent} mouseupEvent - The mouseup event
|
||||
* @param {Object} mdState - The mousedown state captured earlier
|
||||
*/
|
||||
function resolveAfterMouseUp(mouseupEvent, mdState) {
|
||||
const currentMatches = [];
|
||||
let anyHasLongerSequence = false;
|
||||
let foundAnyMatch = false;
|
||||
|
||||
for (const [elementId, data] of MouseRegistry.elements) {
|
||||
const element = document.getElementById(elementId);
|
||||
if (!element) continue;
|
||||
|
||||
// isInside is based on mouseup target position
|
||||
const isInside = element.contains(mouseupEvent.target);
|
||||
|
||||
const treeRoot = data.tree;
|
||||
const currentNode = traverseTree(treeRoot, MouseRegistry.snapshotHistory);
|
||||
|
||||
if (!currentNode) continue;
|
||||
|
||||
foundAnyMatch = true;
|
||||
|
||||
const hasMatch = currentNode.config !== null;
|
||||
const hasLongerSequences = currentNode.children.size > 0;
|
||||
|
||||
if (hasLongerSequences) {
|
||||
anyHasLongerSequence = true;
|
||||
}
|
||||
|
||||
if (hasMatch) {
|
||||
currentMatches.push({
|
||||
elementId: elementId,
|
||||
config: currentNode.config,
|
||||
combinationStr: currentNode.combinationStr,
|
||||
isInside: isInside,
|
||||
hasFocus: mdState.hasFocusMap[elementId] || false,
|
||||
mousedownJsValues: mdState.mousedownJsValues[elementId] || {}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Clear pending matches from before mousedown
|
||||
MouseRegistry.pendingMatches = [];
|
||||
|
||||
// Decision logic (same pattern as handleGlobalClick)
|
||||
if (currentMatches.length > 0 && !anyHasLongerSequence) {
|
||||
for (const match of currentMatches) {
|
||||
triggerMousedownUpAction(match, mouseupEvent);
|
||||
}
|
||||
MouseRegistry.snapshotHistory = [];
|
||||
|
||||
} else if (currentMatches.length > 0 && anyHasLongerSequence) {
|
||||
MouseRegistry.pendingMatches = currentMatches.map(m => ({ ...m, mouseupEvent: mouseupEvent }));
|
||||
|
||||
MouseRegistry.pendingTimeout = setTimeout(() => {
|
||||
for (const match of MouseRegistry.pendingMatches) {
|
||||
triggerMousedownUpAction(match, match.mouseupEvent);
|
||||
}
|
||||
MouseRegistry.snapshotHistory = [];
|
||||
MouseRegistry.pendingMatches = [];
|
||||
MouseRegistry.pendingTimeout = null;
|
||||
}, MouseRegistry.sequenceTimeout);
|
||||
|
||||
} else if (currentMatches.length === 0 && anyHasLongerSequence) {
|
||||
// Wait for more input
|
||||
|
||||
} else {
|
||||
MouseRegistry.snapshotHistory = [];
|
||||
}
|
||||
|
||||
if (!foundAnyMatch) {
|
||||
MouseRegistry.snapshotHistory = [];
|
||||
}
|
||||
|
||||
if (MouseRegistry.snapshotHistory.length > 10) {
|
||||
MouseRegistry.snapshotHistory = [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger an HTMX action for a mousedown>mouseup match.
|
||||
* Similar to triggerHtmxAction but merges suffixed JS values from both phases.
|
||||
* @param {Object} match - The match object with config, elementId, etc.
|
||||
* @param {MouseEvent} mouseupEvent - The mouseup event
|
||||
*/
|
||||
function triggerMousedownUpAction(match, mouseupEvent) {
|
||||
const element = document.getElementById(match.elementId);
|
||||
if (!element) return;
|
||||
|
||||
const config = match.config;
|
||||
|
||||
// Extract HTTP method and URL
|
||||
let method = 'POST';
|
||||
let url = null;
|
||||
|
||||
const methodMap = {
|
||||
'hx-post': 'POST', 'hx-get': 'GET', 'hx-put': 'PUT',
|
||||
'hx-delete': 'DELETE', 'hx-patch': 'PATCH'
|
||||
};
|
||||
|
||||
for (const [attr, httpMethod] of Object.entries(methodMap)) {
|
||||
if (config[attr]) {
|
||||
method = httpMethod;
|
||||
url = config[attr];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!url) {
|
||||
console.error('No HTTP method attribute found in config:', config);
|
||||
return;
|
||||
}
|
||||
|
||||
// Build htmx.ajax options
|
||||
const htmxOptions = {};
|
||||
|
||||
if (config['hx-target']) {
|
||||
htmxOptions.target = config['hx-target'];
|
||||
}
|
||||
|
||||
if (config['hx-swap']) {
|
||||
htmxOptions.swap = config['hx-swap'];
|
||||
}
|
||||
|
||||
// Build values
|
||||
const values = {};
|
||||
|
||||
// 1. Merge static hx-vals from command
|
||||
if (config['hx-vals'] && typeof config['hx-vals'] === 'object') {
|
||||
Object.assign(values, config['hx-vals']);
|
||||
}
|
||||
|
||||
// 2. Merge mousedown JS values (already suffixed with _mousedown)
|
||||
if (match.mousedownJsValues) {
|
||||
Object.assign(values, match.mousedownJsValues);
|
||||
}
|
||||
|
||||
// 3. Call JS function at mouseup time, suffix with _mouseup
|
||||
if (config['hx-vals-extra']) {
|
||||
const extra = config['hx-vals-extra'];
|
||||
|
||||
// Merge static dict values
|
||||
if (extra.dict && typeof extra.dict === 'object') {
|
||||
Object.assign(values, extra.dict);
|
||||
}
|
||||
|
||||
// Call dynamic JS function and suffix with _mouseup
|
||||
if (extra.js) {
|
||||
try {
|
||||
const func = window[extra.js];
|
||||
if (typeof func === 'function') {
|
||||
const dynamicValues = func(mouseupEvent, element, match.combinationStr);
|
||||
if (dynamicValues && typeof dynamicValues === 'object') {
|
||||
for (const [key, value] of Object.entries(dynamicValues)) {
|
||||
values[key + '_mouseup'] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error calling dynamic hx-vals function at mouseup:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Add auto params
|
||||
values.combination = match.combinationStr;
|
||||
values.has_focus = match.hasFocus;
|
||||
values.is_inside = match.isInside;
|
||||
|
||||
htmxOptions.values = values;
|
||||
|
||||
// Add any other hx-* attributes
|
||||
for (const [key, value] of Object.entries(config)) {
|
||||
if (key.startsWith('hx-') && !['hx-post', 'hx-get', 'hx-put', 'hx-delete', 'hx-patch', 'hx-target', 'hx-swap', 'hx-vals', 'hx-vals-extra'].includes(key)) {
|
||||
const optionKey = key.substring(3).replace(/-([a-z])/g, (g) => g[1].toUpperCase());
|
||||
htmxOptions[optionKey] = value;
|
||||
}
|
||||
}
|
||||
|
||||
htmx.ajax(method, url, htmxOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach the global mouse event listeners if not already attached
|
||||
*/
|
||||
@@ -497,9 +965,13 @@
|
||||
// Store handler references for proper removal
|
||||
MouseRegistry.clickHandler = (e) => handleMouseEvent(e, 'click');
|
||||
MouseRegistry.contextmenuHandler = (e) => handleMouseEvent(e, 'right_click');
|
||||
MouseRegistry.mousedownHandler = (e) => handleMouseDown(e);
|
||||
MouseRegistry.mouseupHandler = (e) => handleMouseUp(e);
|
||||
|
||||
document.addEventListener('click', MouseRegistry.clickHandler);
|
||||
document.addEventListener('contextmenu', MouseRegistry.contextmenuHandler);
|
||||
document.addEventListener('mousedown', MouseRegistry.mousedownHandler);
|
||||
document.addEventListener('mouseup', MouseRegistry.mouseupHandler);
|
||||
MouseRegistry.listenerAttached = true;
|
||||
}
|
||||
}
|
||||
@@ -511,11 +983,15 @@
|
||||
if (MouseRegistry.listenerAttached) {
|
||||
document.removeEventListener('click', MouseRegistry.clickHandler);
|
||||
document.removeEventListener('contextmenu', MouseRegistry.contextmenuHandler);
|
||||
document.removeEventListener('mousedown', MouseRegistry.mousedownHandler);
|
||||
document.removeEventListener('mouseup', MouseRegistry.mouseupHandler);
|
||||
MouseRegistry.listenerAttached = false;
|
||||
|
||||
// Clean up handler references
|
||||
MouseRegistry.clickHandler = null;
|
||||
MouseRegistry.contextmenuHandler = null;
|
||||
MouseRegistry.mousedownHandler = null;
|
||||
MouseRegistry.mouseupHandler = null;
|
||||
|
||||
// Clean up all state
|
||||
MouseRegistry.snapshotHistory = [];
|
||||
@@ -524,6 +1000,9 @@
|
||||
MouseRegistry.pendingTimeout = null;
|
||||
}
|
||||
MouseRegistry.pendingMatches = [];
|
||||
clearMousedownState();
|
||||
MouseRegistry.suppressNextClick = false;
|
||||
MouseRegistry.mousedownPending = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -666,6 +666,32 @@ function updateDatagridSelection(datagridId) {
|
||||
document.querySelectorAll(`[data-col="${elementId}"]`).forEach((columnElement) => {
|
||||
columnElement.classList.add('dt2-selected-column');
|
||||
});
|
||||
} else if (selectionType === 'range') {
|
||||
// Parse range tuple string: "(min_col,min_row,max_col,max_row)"
|
||||
// Remove parentheses and split
|
||||
const cleanedId = elementId.replace(/[()]/g, '');
|
||||
const parts = cleanedId.split(',');
|
||||
if (parts.length === 4) {
|
||||
const [minCol, minRow, maxCol, maxRow] = parts;
|
||||
|
||||
// Convert to integers
|
||||
const minColNum = parseInt(minCol);
|
||||
const maxColNum = parseInt(maxCol);
|
||||
const minRowNum = parseInt(minRow);
|
||||
const maxRowNum = parseInt(maxRow);
|
||||
|
||||
// Iterate through range and select cells by reconstructed ID
|
||||
for (let col = minColNum; col <= maxColNum; col++) {
|
||||
for (let row = minRowNum; row <= maxRowNum; row++) {
|
||||
const cellId = `tcell_${datagridId}-${col}-${row}`;
|
||||
const cell = document.getElementById(cellId);
|
||||
if (cell) {
|
||||
cell.classList.add('dt2-selected-cell');
|
||||
cell.style.userSelect = 'text';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user