Added mouse selection
This commit is contained in:
@@ -14,7 +14,15 @@
|
|||||||
pendingMatches: [], // Array of matches waiting for timeout
|
pendingMatches: [], // Array of matches waiting for timeout
|
||||||
sequenceTimeout: 500, // 500ms timeout for sequences
|
sequenceTimeout: 500, // 500ms timeout for sequences
|
||||||
clickHandler: null,
|
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;
|
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
|
* Create a unique string key from a Set of actions for Map indexing
|
||||||
* @param {Set} actionSet - Set of normalized actions
|
* @param {Set} actionSet - Set of normalized actions
|
||||||
@@ -226,6 +286,16 @@
|
|||||||
* @param {MouseEvent} event - The mouse event
|
* @param {MouseEvent} event - The mouse event
|
||||||
*/
|
*/
|
||||||
function handleGlobalClick(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
|
// DEBUG: Measure click handler performance
|
||||||
const clickStart = performance.now();
|
const clickStart = performance.now();
|
||||||
const elementCount = MouseRegistry.elements.size;
|
const elementCount = MouseRegistry.elements.size;
|
||||||
@@ -362,6 +432,10 @@
|
|||||||
* @param {MouseEvent} event - The mouse event
|
* @param {MouseEvent} event - The mouse event
|
||||||
*/
|
*/
|
||||||
function handleElementRightClick(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
|
// Find which registered element was clicked
|
||||||
const elementId = findRegisteredElement(event.target);
|
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
|
* Attach the global mouse event listeners if not already attached
|
||||||
*/
|
*/
|
||||||
@@ -497,9 +965,13 @@
|
|||||||
// Store handler references for proper removal
|
// Store handler references for proper removal
|
||||||
MouseRegistry.clickHandler = (e) => handleMouseEvent(e, 'click');
|
MouseRegistry.clickHandler = (e) => handleMouseEvent(e, 'click');
|
||||||
MouseRegistry.contextmenuHandler = (e) => handleMouseEvent(e, 'right_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('click', MouseRegistry.clickHandler);
|
||||||
document.addEventListener('contextmenu', MouseRegistry.contextmenuHandler);
|
document.addEventListener('contextmenu', MouseRegistry.contextmenuHandler);
|
||||||
|
document.addEventListener('mousedown', MouseRegistry.mousedownHandler);
|
||||||
|
document.addEventListener('mouseup', MouseRegistry.mouseupHandler);
|
||||||
MouseRegistry.listenerAttached = true;
|
MouseRegistry.listenerAttached = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -511,11 +983,15 @@
|
|||||||
if (MouseRegistry.listenerAttached) {
|
if (MouseRegistry.listenerAttached) {
|
||||||
document.removeEventListener('click', MouseRegistry.clickHandler);
|
document.removeEventListener('click', MouseRegistry.clickHandler);
|
||||||
document.removeEventListener('contextmenu', MouseRegistry.contextmenuHandler);
|
document.removeEventListener('contextmenu', MouseRegistry.contextmenuHandler);
|
||||||
|
document.removeEventListener('mousedown', MouseRegistry.mousedownHandler);
|
||||||
|
document.removeEventListener('mouseup', MouseRegistry.mouseupHandler);
|
||||||
MouseRegistry.listenerAttached = false;
|
MouseRegistry.listenerAttached = false;
|
||||||
|
|
||||||
// Clean up handler references
|
// Clean up handler references
|
||||||
MouseRegistry.clickHandler = null;
|
MouseRegistry.clickHandler = null;
|
||||||
MouseRegistry.contextmenuHandler = null;
|
MouseRegistry.contextmenuHandler = null;
|
||||||
|
MouseRegistry.mousedownHandler = null;
|
||||||
|
MouseRegistry.mouseupHandler = null;
|
||||||
|
|
||||||
// Clean up all state
|
// Clean up all state
|
||||||
MouseRegistry.snapshotHistory = [];
|
MouseRegistry.snapshotHistory = [];
|
||||||
@@ -524,6 +1000,9 @@
|
|||||||
MouseRegistry.pendingTimeout = null;
|
MouseRegistry.pendingTimeout = null;
|
||||||
}
|
}
|
||||||
MouseRegistry.pendingMatches = [];
|
MouseRegistry.pendingMatches = [];
|
||||||
|
clearMousedownState();
|
||||||
|
MouseRegistry.suppressNextClick = false;
|
||||||
|
MouseRegistry.mousedownPending = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -666,6 +666,32 @@ function updateDatagridSelection(datagridId) {
|
|||||||
document.querySelectorAll(`[data-col="${elementId}"]`).forEach((columnElement) => {
|
document.querySelectorAll(`[data-col="${elementId}"]`).forEach((columnElement) => {
|
||||||
columnElement.classList.add('dt2-selected-column');
|
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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -152,6 +152,13 @@ class Commands(BaseCommands):
|
|||||||
self._owner.on_click
|
self._owner.on_click
|
||||||
).htmx(target=f"#tsm_{self._id}")
|
).htmx(target=f"#tsm_{self._id}")
|
||||||
|
|
||||||
|
def on_mouse_selection(self):
|
||||||
|
return Command("OnMouseSelection",
|
||||||
|
"Range selection with mouse",
|
||||||
|
self._owner,
|
||||||
|
self._owner.on_mouse_selection
|
||||||
|
).htmx(target=f"#tsm_{self._id}")
|
||||||
|
|
||||||
def toggle_columns_manager(self):
|
def toggle_columns_manager(self):
|
||||||
return Command("ToggleColumnsManager",
|
return Command("ToggleColumnsManager",
|
||||||
"Hide/Show Columns Manager",
|
"Hide/Show Columns Manager",
|
||||||
@@ -231,6 +238,7 @@ class DataGrid(MultipleInstance):
|
|||||||
|
|
||||||
# other definitions
|
# other definitions
|
||||||
self._mouse_support = {
|
self._mouse_support = {
|
||||||
|
"mousedown>mouseup": {"command": self.commands.on_mouse_selection(), "hx_vals": "js:getCellId()"},
|
||||||
"click": {"command": self.commands.on_click(), "hx_vals": "js:getCellId()"},
|
"click": {"command": self.commands.on_click(), "hx_vals": "js:getCellId()"},
|
||||||
"ctrl+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()"},
|
"shift+click": {"command": self.commands.on_click(), "hx_vals": "js:getCellId()"},
|
||||||
@@ -479,12 +487,30 @@ class DataGrid(MultipleInstance):
|
|||||||
def on_click(self, combination, is_inside, cell_id):
|
def on_click(self, combination, is_inside, cell_id):
|
||||||
logger.debug(f"on_click {combination=} {is_inside=} {cell_id=}")
|
logger.debug(f"on_click {combination=} {is_inside=} {cell_id=}")
|
||||||
if is_inside and cell_id:
|
if is_inside and cell_id:
|
||||||
|
self._state.selection.extra_selected.clear()
|
||||||
|
|
||||||
if cell_id.startswith("tcell_"):
|
if cell_id.startswith("tcell_"):
|
||||||
pos = self._get_pos_from_element_id(cell_id)
|
pos = self._get_pos_from_element_id(cell_id)
|
||||||
self._update_current_position(pos)
|
self._update_current_position(pos)
|
||||||
|
|
||||||
return self.render_partial()
|
return self.render_partial()
|
||||||
|
|
||||||
|
def on_mouse_selection(self, combination, is_inside, cell_id_mousedown, cell_id_mouseup):
|
||||||
|
logger.debug(f"on_mouse_selection {combination=} {is_inside=} {cell_id_mousedown=} {cell_id_mouseup=}")
|
||||||
|
if (is_inside and
|
||||||
|
cell_id_mousedown and cell_id_mouseup and
|
||||||
|
cell_id_mousedown.startswith("tcell_") and cell_id_mouseup.startswith("tcell_")):
|
||||||
|
pos_mouse_down = self._get_pos_from_element_id(cell_id_mousedown)
|
||||||
|
pos_mouse_up = self._get_pos_from_element_id(cell_id_mouseup)
|
||||||
|
|
||||||
|
min_col, max_col = min(pos_mouse_down[0], pos_mouse_up[0]), max(pos_mouse_down[0], pos_mouse_up[0])
|
||||||
|
min_row, max_row = min(pos_mouse_down[1], pos_mouse_up[1]), max(pos_mouse_down[1], pos_mouse_up[1])
|
||||||
|
|
||||||
|
self._state.selection.extra_selected.clear()
|
||||||
|
self._state.selection.extra_selected.append(("range", (min_col, min_row, max_col, max_row)))
|
||||||
|
|
||||||
|
return self.render_partial()
|
||||||
|
|
||||||
def on_column_changed(self):
|
def on_column_changed(self):
|
||||||
logger.debug("on_column_changed")
|
logger.debug("on_column_changed")
|
||||||
return self.render_partial("table")
|
return self.render_partial("table")
|
||||||
@@ -752,6 +778,9 @@ class DataGrid(MultipleInstance):
|
|||||||
# selected.append(("cell", self._get_element_id_from_pos("cell", self._state.selection.selected)))
|
# selected.append(("cell", self._get_element_id_from_pos("cell", self._state.selection.selected)))
|
||||||
selected.append(("focus", self._get_element_id_from_pos("cell", self._state.selection.selected)))
|
selected.append(("focus", self._get_element_id_from_pos("cell", self._state.selection.selected)))
|
||||||
|
|
||||||
|
for extra_sel in self._state.selection.extra_selected:
|
||||||
|
selected.append(extra_sel)
|
||||||
|
|
||||||
return Div(
|
return Div(
|
||||||
*[Div(selection_type=s_type, element_id=f"{elt_id}") for s_type, elt_id in selected],
|
*[Div(selection_type=s_type, element_id=f"{elt_id}") for s_type, elt_id in selected],
|
||||||
id=f"tsm_{self._id}",
|
id=f"tsm_{self._id}",
|
||||||
|
|||||||
@@ -19,11 +19,62 @@ class Mouse(MultipleInstance):
|
|||||||
- Both (named params override command): mouse.add("click", command, hx_target="#other")
|
- Both (named params override command): mouse.add("click", command, hx_target="#other")
|
||||||
|
|
||||||
For dynamic hx_vals, use "js:functionName()" to call a client-side function.
|
For dynamic hx_vals, use "js:functionName()" to call a client-side function.
|
||||||
|
|
||||||
|
Supported base actions:
|
||||||
|
- ``click`` - Left mouse click (detected globally)
|
||||||
|
- ``right_click`` (or alias ``rclick``) - Right mouse click (detected on element only)
|
||||||
|
- ``mousedown>mouseup`` - Left mouse press-and-release (captures data at both phases)
|
||||||
|
- ``rmousedown>mouseup`` - Right mouse press-and-release
|
||||||
|
|
||||||
|
Modifiers can be combined with ``+``: ``ctrl+click``, ``shift+mousedown>mouseup``.
|
||||||
|
Sequences use space separation: ``click right_click``, ``click mousedown>mouseup``.
|
||||||
|
|
||||||
|
For ``mousedown>mouseup`` actions with ``hx_vals="js:functionName()"``, the JS function
|
||||||
|
is called at both mousedown and mouseup. Results are suffixed: ``key_mousedown`` and
|
||||||
|
``key_mouseup`` in the server request.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
VALID_ACTIONS = {
|
||||||
|
'click', 'right_click', 'rclick',
|
||||||
|
'mousedown>mouseup', 'rmousedown>mouseup'
|
||||||
|
}
|
||||||
|
VALID_MODIFIERS = {'ctrl', 'shift', 'alt'}
|
||||||
def __init__(self, parent, _id=None, combinations=None):
|
def __init__(self, parent, _id=None, combinations=None):
|
||||||
super().__init__(parent, _id=_id)
|
super().__init__(parent, _id=_id)
|
||||||
self.combinations = combinations or {}
|
self.combinations = combinations or {}
|
||||||
|
|
||||||
|
def _validate_sequence(self, sequence: str):
|
||||||
|
"""
|
||||||
|
Validate a mouse event sequence string.
|
||||||
|
|
||||||
|
Checks that all elements in the sequence use valid action names and modifiers.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sequence: Mouse event sequence string (e.g., "click", "ctrl+mousedown>mouseup")
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If any action or modifier is invalid.
|
||||||
|
"""
|
||||||
|
elements = sequence.strip().split()
|
||||||
|
for element in elements:
|
||||||
|
parts = element.split('+')
|
||||||
|
# Last part should be the action, others are modifiers
|
||||||
|
action = parts[-1].lower()
|
||||||
|
modifiers = [p.lower() for p in parts[:-1]]
|
||||||
|
|
||||||
|
if action not in self.VALID_ACTIONS:
|
||||||
|
raise ValueError(
|
||||||
|
f"Invalid action '{action}' in sequence '{sequence}'. "
|
||||||
|
f"Valid actions: {', '.join(sorted(self.VALID_ACTIONS))}"
|
||||||
|
)
|
||||||
|
|
||||||
|
for mod in modifiers:
|
||||||
|
if mod not in self.VALID_MODIFIERS:
|
||||||
|
raise ValueError(
|
||||||
|
f"Invalid modifier '{mod}' in sequence '{sequence}'. "
|
||||||
|
f"Valid modifiers: {', '.join(sorted(self.VALID_MODIFIERS))}"
|
||||||
|
)
|
||||||
|
|
||||||
def add(self, sequence: str, command: Command = None, *,
|
def add(self, sequence: str, command: Command = None, *,
|
||||||
hx_post: str = None, hx_get: str = None, hx_put: str = None,
|
hx_post: str = None, hx_get: str = None, hx_put: str = None,
|
||||||
hx_delete: str = None, hx_patch: str = None,
|
hx_delete: str = None, hx_patch: str = None,
|
||||||
@@ -32,7 +83,11 @@ class Mouse(MultipleInstance):
|
|||||||
Add a mouse combination with optional command and HTMX parameters.
|
Add a mouse combination with optional command and HTMX parameters.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
sequence: Mouse event sequence (e.g., "click", "ctrl+click", "click right_click")
|
sequence: Mouse event sequence string. Supports:
|
||||||
|
- Simple actions: ``"click"``, ``"right_click"``, ``"mousedown>mouseup"``
|
||||||
|
- Modifiers: ``"ctrl+click"``, ``"shift+mousedown>mouseup"``
|
||||||
|
- Sequences: ``"click right_click"``, ``"click mousedown>mouseup"``
|
||||||
|
- Aliases: ``"rclick"`` for ``"right_click"``
|
||||||
command: Optional Command object for server-side action
|
command: Optional Command object for server-side action
|
||||||
hx_post: HTMX post URL (overrides command)
|
hx_post: HTMX post URL (overrides command)
|
||||||
hx_get: HTMX get URL (overrides command)
|
hx_get: HTMX get URL (overrides command)
|
||||||
@@ -41,11 +96,17 @@ class Mouse(MultipleInstance):
|
|||||||
hx_patch: HTMX patch URL (overrides command)
|
hx_patch: HTMX patch URL (overrides command)
|
||||||
hx_target: HTMX target selector (overrides command)
|
hx_target: HTMX target selector (overrides command)
|
||||||
hx_swap: HTMX swap strategy (overrides command)
|
hx_swap: HTMX swap strategy (overrides command)
|
||||||
hx_vals: HTMX values dict or "js:functionName()" for dynamic values
|
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``.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
self for method chaining
|
self for method chaining
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If the sequence contains invalid actions or modifiers.
|
||||||
"""
|
"""
|
||||||
|
self._validate_sequence(sequence)
|
||||||
self.combinations[sequence] = {
|
self.combinations[sequence] = {
|
||||||
"command": command,
|
"command": command,
|
||||||
"hx_post": hx_post,
|
"hx_post": hx_post,
|
||||||
|
|||||||
@@ -30,10 +30,15 @@ class DatagridEditionState:
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class DatagridSelectionState:
|
class DatagridSelectionState:
|
||||||
|
"""
|
||||||
|
element_id: str
|
||||||
|
"tcell_grid_id_col_row" for cell
|
||||||
|
(min_col, min_row, max_col, max_row) for range
|
||||||
|
"""
|
||||||
selected: tuple[int, int] | None = None # column first, then row
|
selected: tuple[int, int] | None = None # column first, then row
|
||||||
last_selected: tuple[int, int] | None = None
|
last_selected: tuple[int, int] | None = None
|
||||||
selection_mode: str = None # valid values are "row", "column" or None for "cell"
|
selection_mode: str = None # valid values are "row", "column", "range" or None for "cell"
|
||||||
extra_selected: list[tuple[str, str | int]] = field(default_factory=list) # list(tuple(selection_mode, element_id))
|
extra_selected: list[tuple[str, str | int | tuple]] = field(default_factory=list) # (selection_mode, element_id)
|
||||||
last_extra_selected: tuple[int, int] = None
|
last_extra_selected: tuple[int, int] = None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -125,8 +125,6 @@ class Command:
|
|||||||
|
|
||||||
def execute(self, client_response: dict = None):
|
def execute(self, client_response: dict = None):
|
||||||
logger.debug(f"Executing command {self.name} with arguments {client_response=}")
|
logger.debug(f"Executing command {self.name} with arguments {client_response=}")
|
||||||
if self._htmx_extra.get("hx-target", "").startswith("#tsm_"):
|
|
||||||
logger.warning(f" Command {self.name} needs a selection manager to work properly.")
|
|
||||||
with ObservableResultCollector(self._bindings) as collector:
|
with ObservableResultCollector(self._bindings) as collector:
|
||||||
kwargs = self._create_kwargs(self.default_kwargs,
|
kwargs = self._create_kwargs(self.default_kwargs,
|
||||||
client_response,
|
client_response,
|
||||||
@@ -150,10 +148,6 @@ class Command:
|
|||||||
and r.get("id", None) is not None):
|
and r.get("id", None) is not None):
|
||||||
r.attrs["hx-swap-oob"] = r.attrs.get("hx-swap-oob", "true")
|
r.attrs["hx-swap-oob"] = r.attrs.get("hx-swap-oob", "true")
|
||||||
|
|
||||||
if self._htmx_extra.get("hx-target", "").startswith("#tsm_"):
|
|
||||||
ret_debug = [f"<{r.tag} id={r.attrs.get('id', '')}/>" if r else "None" for r in all_ret]
|
|
||||||
logger.warning(f" {ret_debug=}")
|
|
||||||
|
|
||||||
return all_ret[0] if len(all_ret) == 1 else all_ret
|
return all_ret[0] if len(all_ret) == 1 else all_ret
|
||||||
|
|
||||||
def htmx(self, target: Optional[str] = "this", swap="outerHTML", trigger=None, auto_swap_oob=True):
|
def htmx(self, target: Optional[str] = "this", swap="outerHTML", trigger=None, auto_swap_oob=True):
|
||||||
|
|||||||
@@ -159,6 +159,14 @@
|
|||||||
<li><code>rclick</code> - Right click (using rclick alias)</li>
|
<li><code>rclick</code> - Right click (using rclick alias)</li>
|
||||||
<li><code>click rclick</code> - Click then right-click sequence (using alias)</li>
|
<li><code>click rclick</code> - Click then right-click sequence (using alias)</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
<p><strong>Element 3 - Mousedown>mouseup actions:</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li><code>click</code> - Simple click (coexists with mousedown>mouseup)</li>
|
||||||
|
<li><code>mousedown>mouseup</code> - Left press and release (with JS values)</li>
|
||||||
|
<li><code>ctrl+mousedown>mouseup</code> - Ctrl + press and release</li>
|
||||||
|
<li><code>rmousedown>mouseup</code> - Right press and release</li>
|
||||||
|
<li><code>click mousedown>mouseup</code> - Click then press-and-release sequence</li>
|
||||||
|
</ul>
|
||||||
<p><strong>Note:</strong> <code>rclick</code> is an alias for <code>right_click</code> and works identically.</p>
|
<p><strong>Note:</strong> <code>rclick</code> is an alias for <code>right_click</code> and works identically.</p>
|
||||||
<p><strong>Tip:</strong> Try different click combinations! Right-click menu will be blocked on test elements.</p>
|
<p><strong>Tip:</strong> Try different click combinations! Right-click menu will be blocked on test elements.</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -197,6 +205,14 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="test-container">
|
||||||
|
<h2>Test Element 3 (Mousedown>mouseup actions)</h2>
|
||||||
|
<div id="test-element-3" class="test-element" tabindex="0">
|
||||||
|
Try mousedown>mouseup actions here!<br>
|
||||||
|
Press and hold, then release. Also try Ctrl+drag, right-drag, and click then drag.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="test-container">
|
<div class="test-container">
|
||||||
<h2>Test Input (normal clicking should work here)</h2>
|
<h2>Test Input (normal clicking should work here)</h2>
|
||||||
<input type="text" placeholder="Try clicking, right-clicking here - should work normally"
|
<input type="text" placeholder="Try clicking, right-clicking here - should work normally"
|
||||||
@@ -346,10 +362,48 @@
|
|||||||
|
|
||||||
add_mouse_support('test-element-2', JSON.stringify(combinations2));
|
add_mouse_support('test-element-2', JSON.stringify(combinations2));
|
||||||
|
|
||||||
|
// JS function for dynamic mousedown>mouseup values
|
||||||
|
// Returns cell-like data for testing
|
||||||
|
window.getCellId = function(event, element, combination) {
|
||||||
|
const x = event.clientX;
|
||||||
|
const y = event.clientY;
|
||||||
|
return {
|
||||||
|
cell_id: `cell-${Math.floor(x / 50)}-${Math.floor(y / 50)}`,
|
||||||
|
x: x,
|
||||||
|
y: y
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Element 3 - Mousedown>mouseup actions
|
||||||
|
const combinations3 = {
|
||||||
|
"click": {
|
||||||
|
"hx-post": "/test/element3-click"
|
||||||
|
},
|
||||||
|
"mousedown>mouseup": {
|
||||||
|
"hx-post": "/test/element3-mousedown-mouseup",
|
||||||
|
"hx-vals-extra": {"js": "getCellId"}
|
||||||
|
},
|
||||||
|
"ctrl+mousedown>mouseup": {
|
||||||
|
"hx-post": "/test/element3-ctrl-mousedown-mouseup",
|
||||||
|
"hx-vals-extra": {"js": "getCellId"}
|
||||||
|
},
|
||||||
|
"rmousedown>mouseup": {
|
||||||
|
"hx-post": "/test/element3-rmousedown-mouseup",
|
||||||
|
"hx-vals-extra": {"js": "getCellId"}
|
||||||
|
},
|
||||||
|
"click mousedown>mouseup": {
|
||||||
|
"hx-post": "/test/element3-click-then-mousedown-mouseup",
|
||||||
|
"hx-vals-extra": {"js": "getCellId"}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
add_mouse_support('test-element-3', JSON.stringify(combinations3));
|
||||||
|
|
||||||
// Log initial state
|
// Log initial state
|
||||||
logEvent('Mouse support initialized',
|
logEvent('Mouse support initialized',
|
||||||
'Element 1: All mouse actions configured',
|
'Element 1: All mouse actions configured',
|
||||||
'Element 2: Using "rclick" alias (click, rclick, and click rclick sequence)',
|
'Element 2: Using "rclick" alias (click, rclick, and click rclick sequence)',
|
||||||
|
'Element 3: Mousedown>mouseup actions (with JS getCellId function)',
|
||||||
'Smart timeout: 500ms for sequences', false);
|
'Smart timeout: 500ms for sequences', false);
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Reference in New Issue
Block a user