fix(widget): restore strength drag on lora entries and header

widget.value is a getter/setter that returns a new array on every read,
so handleStrengthDrag with updateWidget=false mutated a discarded copy.
Introduce __dragActive flag to suppress renderLoras in setValue during
drag, allowing mutations to persist through widget.value without
destroying the DOM. Use try-finally to guarantee flag cleanup.
This commit is contained in:
Will Miao
2026-07-23 08:31:34 +08:00
parent 7c8dc57d55
commit 7b8b778f83
2 changed files with 62 additions and 39 deletions

View File

@@ -751,7 +751,11 @@ export function addLorasWidget(node, name, opts, callback) {
} }
} }
renderLoras(widgetValue, widget); // Skip DOM re-render during drag to preserve pointer capture and event listeners.
// The strength inputs are updated directly via the pointermove handler instead.
if (!widget.__dragActive) {
renderLoras(widgetValue, widget);
}
}, },
hideOnZoom: true, hideOnZoom: true,
selectOn: ['click', 'focus'] selectOn: ['click', 'focus']

View File

@@ -37,17 +37,18 @@ export function handleStrengthDrag(name, initialStrength, initialX, event, widge
syncClipStrengthIfCollapsed(lorasData[loraIndex]); syncClipStrengthIfCollapsed(lorasData[loraIndex]);
} }
// Update the widget value only if updateWidget flag is true // Always write back to widget.value to persist the mutation.
// This allows us to update inputs directly during drag without triggering re-render // During drag (updateWidget=false), setValue skips renderLoras via __dragActive flag,
if (updateWidget) { // so the DOM survives and pointer capture is preserved.
widget.value = formatLoraValue(lorasData); widget.value = formatLoraValue(lorasData);
}
// Force re-render via callback only if updateWidget is true // Only fire callback on the final commit, not during drag
if (updateWidget && widget.callback) { if (updateWidget && widget.callback) {
widget.callback(widget.value); widget.callback(widget.value);
} }
} }
return newStrength;
} }
// Function to handle proportional strength adjustment for all LoRAs via header dragging // Function to handle proportional strength adjustment for all LoRAs via header dragging
@@ -90,12 +91,11 @@ export function handleAllStrengthsDrag(initialStrengths, initialX, event, widget
lorasData[index].clipStrength = Number(newClipStrength); lorasData[index].clipStrength = Number(newClipStrength);
}); });
// Update widget value only if updateWidget flag is true // Always write back to widget.value to persist mutations.
if (updateWidget) { // During drag (updateWidget=false), setValue skips renderLoras via __dragActive flag.
widget.value = formatLoraValue(lorasData); widget.value = formatLoraValue(lorasData);
}
// Force re-render via callback only if updateWidget is true // Only fire callback on the final commit, not during drag
if (updateWidget && widget.callback) { if (updateWidget && widget.callback) {
widget.callback(widget.value); widget.callback(widget.value);
} }
@@ -149,6 +149,13 @@ export function initDrag(
activePointerId = e.pointerId; activePointerId = e.pointerId;
currentDragElement = e.currentTarget; currentDragElement = e.currentTarget;
// Suppress renderLoras in setValue during drag so the DOM survives.
// The getter creates a new array on every read, so mutations to a
// parsed copy are lost unless we write back through widget.value.
// Writing back would normally trigger a full DOM re-render via setValue,
// destroying pointer capture. __dragActive tells setValue to skip the render.
widget.__dragActive = true;
// Capture pointer to receive all subsequent events regardless of stopPropagation // Capture pointer to receive all subsequent events regardless of stopPropagation
const target = e.currentTarget; const target = e.currentTarget;
target.setPointerCapture(e.pointerId); target.setPointerCapture(e.pointerId);
@@ -181,17 +188,12 @@ export function initDrag(
} }
// Call the strength adjustment function without updating widget.value during drag // Call the strength adjustment function without updating widget.value during drag
handleStrengthDrag(name, initialStrength, initialX, e, widget, isClipStrength, false); const newStrength = handleStrengthDrag(name, initialStrength, initialX, e, widget, isClipStrength, false);
// Update strength input directly instead of re-rendering to avoid losing event listeners // Update strength input directly instead of re-rendering to avoid losing event listeners
const strengthInput = currentDragElement.querySelector('.lm-lora-strength-input'); const strengthInput = currentDragElement.querySelector('.lm-lora-strength-input');
if (strengthInput) { if (strengthInput && typeof newStrength === 'number') {
const lorasData = parseLoraValue(widget.value); strengthInput.value = newStrength.toFixed(2);
const loraData = lorasData.find(l => l.name === name);
if (loraData) {
const strengthValue = isClipStrength ? loraData.clipStrength : loraData.strength;
strengthInput.value = Number(strengthValue).toFixed(2);
}
} }
// Prevent showing the preview tooltip during drag // Prevent showing the preview tooltip during drag
@@ -226,20 +228,27 @@ export function initDrag(
// 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');
// Only call onDragEnd and re-render if we actually dragged // Only call onDragEnd and re-render if we actually dragged.
if (wasDragging) { // try-finally guarantees __dragActive is always cleared, preventing a
if (typeof onDragEnd === 'function') { // permanent UI freeze if onDragEnd or setValue throws during cleanup.
onDragEnd(); try {
} if (wasDragging) {
if (typeof onDragEnd === 'function') {
onDragEnd();
}
// Commit final value through options.setValue so external observers are notified. // Re-enable renderLoras in setValue and flush final value through setter.
// During drag, handleStrengthDrag mutates widgetValue in-place (updateWidget=false), // The last handleStrengthDrag call already wrote the final strength to
// bypassing widget.value setter and options.setValue entirely. This assignment // widgetValue via setValue (with render suppressed). widget.value = widget.value
// flushes the in-place mutation through the setter so any setValue wrappers fire. // triggers setValue again, which now calls renderLoras since __dragActive is false.
widget.value = widget.value; widget.__dragActive = false;
if (typeof widget.callback === 'function') { widget.value = widget.value;
widget.callback(widget.value); if (typeof widget.callback === 'function') {
widget.callback(widget.value);
}
} }
} finally {
widget.__dragActive = false;
} }
}; };
@@ -285,6 +294,9 @@ export function initHeaderDrag(headerEl, widget, renderFunction) {
activePointerId = e.pointerId; activePointerId = e.pointerId;
currentHeaderElement = e.currentTarget; currentHeaderElement = e.currentTarget;
// Suppress renderLoras in setValue during drag (see initDrag for rationale)
widget.__dragActive = true;
// Capture pointer to receive all subsequent events regardless of stopPropagation // Capture pointer to receive all subsequent events regardless of stopPropagation
const target = e.currentTarget; const target = e.currentTarget;
target.setPointerCapture(e.pointerId); target.setPointerCapture(e.pointerId);
@@ -352,13 +364,20 @@ export function initHeaderDrag(headerEl, widget, renderFunction) {
// 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');
// Only re-render if we actually dragged // Only re-render if we actually dragged.
if (wasDragging) { // try-finally guarantees __dragActive is always cleared, preventing a
// Commit final value through options.setValue so external observers are notified. // permanent UI freeze if setValue throws during cleanup.
widget.value = widget.value; try {
if (typeof widget.callback === 'function') { if (wasDragging) {
widget.callback(widget.value); // Re-enable renderLoras in setValue and flush final value through setter
widget.__dragActive = false;
widget.value = widget.value;
if (typeof widget.callback === 'function') {
widget.callback(widget.value);
}
} }
} finally {
widget.__dragActive = false;
} }
}; };