feat(loras): add drag event callbacks and preview suppression

- Add onDragStart and onDragEnd callbacks to initDrag function
- Implement preview suppression during and briefly after strength dragging
- Clear preview timer on drag start/end to prevent tooltip conflicts
- Update tests to verify drag callbacks are properly triggered

This prevents tooltip previews from interfering with drag interactions and provides better control over drag lifecycle events.
This commit is contained in:
Will Miao
2025-11-03 12:18:59 +08:00
parent 707d0cb8a4
commit 30f9e3e2ec
3 changed files with 83 additions and 10 deletions

View File

@@ -82,6 +82,8 @@ describe('LoRA widget drag interactions', () => {
const module = await import(EVENTS_MODULE); const module = await import(EVENTS_MODULE);
const renderSpy = vi.fn(); const renderSpy = vi.fn();
const previewSpy = { hide: vi.fn() }; const previewSpy = { hide: vi.fn() };
const onDragStart = vi.fn();
const onDragEnd = vi.fn();
const dragEl = document.createElement('div'); const dragEl = document.createElement('div');
dragEl.className = 'lm-lora-entry'; dragEl.className = 'lm-lora-entry';
@@ -92,10 +94,14 @@ describe('LoRA widget drag interactions', () => {
callback: vi.fn(), callback: vi.fn(),
}; };
module.initDrag(dragEl, 'Test', widget, false, previewSpy, renderSpy); module.initDrag(dragEl, 'Test', widget, false, previewSpy, renderSpy, {
onDragStart,
onDragEnd,
});
dragEl.dispatchEvent(new MouseEvent('mousedown', { clientX: 50, bubbles: true })); dragEl.dispatchEvent(new MouseEvent('mousedown', { clientX: 50, bubbles: true }));
expect(document.body.classList.contains('lm-lora-strength-dragging')).toBe(true); expect(document.body.classList.contains('lm-lora-strength-dragging')).toBe(true);
expect(onDragStart).toHaveBeenCalledTimes(1);
document.dispatchEvent(new MouseEvent('mousemove', { clientX: 70, bubbles: true })); document.dispatchEvent(new MouseEvent('mousemove', { clientX: 70, bubbles: true }));
expect(renderSpy).toHaveBeenCalledWith(widget.value, widget); expect(renderSpy).toHaveBeenCalledWith(widget.value, widget);
@@ -104,6 +110,7 @@ describe('LoRA widget drag interactions', () => {
document.dispatchEvent(new MouseEvent('mouseup')); document.dispatchEvent(new MouseEvent('mouseup'));
expect(document.body.classList.contains('lm-lora-strength-dragging')).toBe(false); expect(document.body.classList.contains('lm-lora-strength-dragging')).toBe(false);
expect(onDragEnd).toHaveBeenCalledTimes(1);
}); });
it('deletes the selected LoRA when backspace is pressed outside of strength inputs', async () => { it('deletes the selected LoRA when backspace is pressed outside of strength inputs', async () => {

View File

@@ -36,6 +36,28 @@ export function addLorasWidget(node, name, opts, callback) {
// Selection state - only one LoRA can be selected at a time // Selection state - only one LoRA can be selected at a time
let selectedLora = null; let selectedLora = null;
let pendingFocusTarget = null; let pendingFocusTarget = null;
const PREVIEW_SUPPRESSION_AFTER_DRAG_MS = 500;
let strengthDragActive = false;
let lastStrengthDragEndAt = 0;
const shouldSuppressPreview = () => {
if (strengthDragActive) {
return true;
}
return Date.now() - lastStrengthDragEndAt < PREVIEW_SUPPRESSION_AFTER_DRAG_MS;
};
const markStrengthDragStart = () => {
strengthDragActive = true;
previewTooltip.hide();
};
const markStrengthDragEnd = () => {
strengthDragActive = false;
lastStrengthDragEndAt = Date.now();
previewTooltip.hide();
};
// Function to select a LoRA // Function to select a LoRA
const selectLora = (loraName) => { const selectLora = (loraName) => {
@@ -259,23 +281,47 @@ export function addLorasWidget(node, name, opts, callback) {
nameEl.className = "lm-lora-name"; nameEl.className = "lm-lora-name";
// Move preview tooltip events to nameEl instead of loraEl // Move preview tooltip events to nameEl instead of loraEl
let previewTimer; // Timer for delayed preview let previewTimer = null; // Timer for delayed preview
nameEl.addEventListener('mouseenter', async (e) => {
const clearPreviewTimer = () => {
if (previewTimer) {
clearTimeout(previewTimer);
previewTimer = null;
}
};
nameEl.addEventListener('mouseenter', (e) => {
e.stopPropagation(); e.stopPropagation();
const rect = nameEl.getBoundingClientRect(); if (shouldSuppressPreview()) {
return;
}
previewTimer = setTimeout(async () => { previewTimer = setTimeout(async () => {
previewTimer = null;
if (shouldSuppressPreview()) {
return;
}
const rect = nameEl.getBoundingClientRect();
await previewTooltip.show(name, rect.right, rect.top); await previewTooltip.show(name, rect.right, rect.top);
}, 400); // 400ms delay }, 400); // 400ms delay
}); });
nameEl.addEventListener('mouseleave', (e) => { nameEl.addEventListener('mouseleave', (e) => {
e.stopPropagation(); e.stopPropagation();
clearTimeout(previewTimer); // Cancel if not triggered clearPreviewTimer(); // Cancel if not triggered
previewTooltip.hide(); previewTooltip.hide();
}); });
// Initialize drag functionality for strength adjustment // Initialize drag functionality for strength adjustment
initDrag(loraEl, name, widget, false, previewTooltip, renderLoras); initDrag(loraEl, name, widget, false, previewTooltip, renderLoras, {
onDragStart: () => {
clearPreviewTimer();
markStrengthDragStart();
},
onDragEnd: () => {
clearPreviewTimer();
markStrengthDragEnd();
}
});
// Add context menu event // Add context menu event
loraEl.addEventListener('contextmenu', (e) => { loraEl.addEventListener('contextmenu', (e) => {
@@ -511,7 +557,10 @@ export function addLorasWidget(node, name, opts, callback) {
clipEl.appendChild(clipStrengthControl); clipEl.appendChild(clipStrengthControl);
// Add drag functionality to clip entry // Add drag functionality to clip entry
initDrag(clipEl, name, widget, true, previewTooltip, renderLoras); initDrag(clipEl, name, widget, true, previewTooltip, renderLoras, {
onDragStart: markStrengthDragStart,
onDragEnd: markStrengthDragEnd
});
container.appendChild(clipEl); container.appendChild(clipEl);
} }

View File

@@ -97,10 +97,19 @@ export function handleAllStrengthsDrag(initialStrengths, initialX, event, widget
} }
// Function to initialize drag operation // Function to initialize drag operation
export function initDrag(dragEl, name, widget, isClipStrength = false, previewTooltip, renderFunction) { export function initDrag(
dragEl,
name,
widget,
isClipStrength = false,
previewTooltip,
renderFunction,
dragCallbacks = {}
) {
let isDragging = false; let isDragging = false;
let initialX = 0; let initialX = 0;
let initialStrength = 0; let initialStrength = 0;
const { onDragStart, onDragEnd } = dragCallbacks;
// Create a drag handler // Create a drag handler
dragEl.addEventListener('mousedown', (e) => { dragEl.addEventListener('mousedown', (e) => {
@@ -122,10 +131,14 @@ export function initDrag(dragEl, name, widget, isClipStrength = false, previewTo
initialX = e.clientX; initialX = e.clientX;
initialStrength = isClipStrength ? loraData.clipStrength : loraData.strength; initialStrength = isClipStrength ? loraData.clipStrength : loraData.strength;
isDragging = true; isDragging = true;
// Add class to body to enforce cursor style globally // Add class to body to enforce cursor style globally
document.body.classList.add('lm-lora-strength-dragging'); document.body.classList.add('lm-lora-strength-dragging');
if (typeof onDragStart === 'function') {
onDragStart();
}
// Prevent text selection during drag // Prevent text selection during drag
e.preventDefault(); e.preventDefault();
}); });
@@ -154,6 +167,10 @@ export function initDrag(dragEl, name, widget, isClipStrength = false, previewTo
isDragging = false; isDragging = false;
// Remove the class to restore normal cursor behavior // Remove the class to restore normal cursor behavior
document.body.classList.remove('lm-lora-strength-dragging'); document.body.classList.remove('lm-lora-strength-dragging');
if (typeof onDragEnd === 'function') {
onDragEnd();
}
} }
}); });
} }