mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 13:12:12 -03:00
Implements issue #808 - Allow users to customize the strength variation range for LoRA widget arrow buttons. Changes: - Add 'Strength Adjustment Step' setting (0.01-0.1) in settings.js - Replace hardcoded 0.05 increments with configurable step value - Apply to both LoRA strength and CLIP strength controls Fixes #808
746 lines
25 KiB
JavaScript
746 lines
25 KiB
JavaScript
import { createToggle, createArrowButton, createDragHandle, updateEntrySelection, createExpandButton, updateExpandButtonState, createLockButton, updateLockButtonState } from "./loras_widget_components.js";
|
|
import {
|
|
parseLoraValue,
|
|
formatLoraValue,
|
|
updateWidgetHeight,
|
|
shouldShowClipEntry,
|
|
syncClipStrengthIfCollapsed,
|
|
LORA_ENTRY_HEIGHT,
|
|
HEADER_HEIGHT,
|
|
CONTAINER_PADDING,
|
|
EMPTY_CONTAINER_HEIGHT
|
|
} from "./loras_widget_utils.js";
|
|
import { initDrag, createContextMenu, initHeaderDrag, initReorderDrag, handleKeyboardNavigation } from "./loras_widget_events.js";
|
|
import { forwardMiddleMouseToCanvas } from "./utils.js";
|
|
import { PreviewTooltip } from "./preview_tooltip.js";
|
|
import { ensureLmStyles } from "./lm_styles_loader.js";
|
|
import { getStrengthStepPreference } from "./settings.js";
|
|
|
|
export function addLorasWidget(node, name, opts, callback) {
|
|
ensureLmStyles();
|
|
|
|
// Create container for loras
|
|
const container = document.createElement("div");
|
|
container.className = "lm-loras-container";
|
|
|
|
forwardMiddleMouseToCanvas(container);
|
|
|
|
// Set initial height using CSS variables approach
|
|
const defaultHeight = 200;
|
|
|
|
// Check if this is a randomizer node (lock button instead of drag handle)
|
|
const isRandomizerNode = opts?.isRandomizerNode === true;
|
|
|
|
// Initialize default value
|
|
const defaultValue = opts?.defaultVal || [];
|
|
const onSelectionChange = typeof opts?.onSelectionChange === "function"
|
|
? opts.onSelectionChange
|
|
: null;
|
|
|
|
// Create preview tooltip instance
|
|
const previewTooltip = new PreviewTooltip({ modelType: "loras" });
|
|
|
|
// Selection state - only one LoRA can be selected at a time
|
|
let selectedLora = null;
|
|
let currentLorasData = parseLoraValue(defaultValue);
|
|
let lastSelectionKey = "__none__";
|
|
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
|
|
const buildSelectionPayload = (loraName) => {
|
|
if (!loraName) {
|
|
return null;
|
|
}
|
|
|
|
const entry = currentLorasData.find((lora) => lora.name === loraName);
|
|
if (!entry) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
name: entry.name,
|
|
active: !!entry.active,
|
|
entry: { ...entry },
|
|
};
|
|
};
|
|
|
|
const emitSelectionChange = (payload, options = {}) => {
|
|
if (!onSelectionChange) {
|
|
return;
|
|
}
|
|
|
|
const key = payload
|
|
? `${payload.name || ""}|${payload.active ? "1" : "0"}`
|
|
: "__null__";
|
|
|
|
if (!options.force && key === lastSelectionKey) {
|
|
return;
|
|
}
|
|
|
|
lastSelectionKey = key;
|
|
onSelectionChange(payload);
|
|
};
|
|
|
|
const selectLora = (loraName, options = {}) => {
|
|
selectedLora = loraName;
|
|
// Update visual feedback for all entries
|
|
container.querySelectorAll('.lm-lora-entry').forEach(entry => {
|
|
const entryLoraName = entry.dataset.loraName;
|
|
updateEntrySelection(entry, entryLoraName === selectedLora);
|
|
});
|
|
|
|
if (!options.silent) {
|
|
emitSelectionChange(buildSelectionPayload(loraName));
|
|
}
|
|
};
|
|
|
|
// Add keyboard event listener to container
|
|
container.addEventListener('keydown', (e) => {
|
|
if (handleKeyboardNavigation(e, selectedLora, widget, renderLoras, selectLora)) {
|
|
e.stopPropagation();
|
|
}
|
|
});
|
|
|
|
// Make container focusable for keyboard events
|
|
container.tabIndex = 0;
|
|
|
|
// Function to render loras from data
|
|
const renderLoras = (value, widget) => {
|
|
// Clear existing content
|
|
while (container.firstChild) {
|
|
container.removeChild(container.firstChild);
|
|
}
|
|
|
|
// Parse the loras data
|
|
const lorasData = parseLoraValue(value);
|
|
currentLorasData = lorasData;
|
|
const focusSequence = [];
|
|
|
|
const updateWidgetValue = (newValue) => {
|
|
widget.value = newValue;
|
|
|
|
if (typeof widget.callback === "function") {
|
|
widget.callback(widget.value);
|
|
}
|
|
};
|
|
|
|
const createFocusEntry = (loraName, type) => {
|
|
const entry = { name: loraName, type };
|
|
focusSequence.push(entry);
|
|
return entry;
|
|
};
|
|
|
|
const findFocusEntryIndex = (entry) =>
|
|
focusSequence.findIndex(
|
|
(sequenceEntry) =>
|
|
sequenceEntry?.name === entry?.name && sequenceEntry?.type === entry?.type
|
|
);
|
|
|
|
const getAdjacentFocusEntry = (currentEntry, direction) => {
|
|
const currentIndex = findFocusEntryIndex(currentEntry);
|
|
if (currentIndex === -1) {
|
|
return null;
|
|
}
|
|
return focusSequence[currentIndex + direction] || null;
|
|
};
|
|
|
|
const queueFocusEntry = (entry) => {
|
|
if (!entry) {
|
|
return false;
|
|
}
|
|
pendingFocusTarget = { ...entry };
|
|
return true;
|
|
};
|
|
|
|
const queueFocusAdjacentFrom = (currentEntry, direction) => {
|
|
const targetEntry = getAdjacentFocusEntry(currentEntry, direction);
|
|
return queueFocusEntry(targetEntry);
|
|
};
|
|
|
|
const escapeLoraName = (loraName) => {
|
|
const css =
|
|
(typeof window !== "undefined" && window.CSS) ||
|
|
(typeof globalThis !== "undefined" && globalThis.CSS);
|
|
if (css && typeof css.escape === "function") {
|
|
return css.escape(loraName);
|
|
}
|
|
return loraName.replace(/"|\\/g, "\\$&");
|
|
};
|
|
|
|
if (lorasData.length === 0) {
|
|
// Show message when no loras are added
|
|
const emptyMessage = document.createElement("div");
|
|
emptyMessage.textContent = "No LoRAs added";
|
|
emptyMessage.className = "lm-lora-empty-state";
|
|
container.appendChild(emptyMessage);
|
|
|
|
// Set fixed height for empty state
|
|
updateWidgetHeight(container, EMPTY_CONTAINER_HEIGHT, defaultHeight, node);
|
|
return;
|
|
}
|
|
|
|
// Create header
|
|
const header = document.createElement("div");
|
|
header.className = "lm-loras-header";
|
|
|
|
// Add toggle all control
|
|
const allActive = lorasData.every(lora => lora.active);
|
|
const toggleAll = createToggle(allActive, (active) => {
|
|
// Update all loras active state
|
|
const lorasData = parseLoraValue(widget.value);
|
|
lorasData.forEach(lora => lora.active = active);
|
|
|
|
const newValue = formatLoraValue(lorasData);
|
|
updateWidgetValue(newValue);
|
|
});
|
|
|
|
// Add label to toggle all
|
|
const toggleLabel = document.createElement("div");
|
|
toggleLabel.textContent = "Toggle All";
|
|
toggleLabel.className = "lm-toggle-label";
|
|
|
|
const toggleContainer = document.createElement("div");
|
|
toggleContainer.className = "lm-toggle-container";
|
|
toggleContainer.appendChild(toggleAll);
|
|
toggleContainer.appendChild(toggleLabel);
|
|
|
|
// Strength label with drag hint
|
|
const strengthLabel = document.createElement("div");
|
|
strengthLabel.textContent = "Strength";
|
|
strengthLabel.className = "lm-strength-label";
|
|
|
|
// Add drag hint icon next to strength label
|
|
const dragHint = document.createElement("span");
|
|
dragHint.innerHTML = "↔"; // Simple left-right arrow as drag indicator
|
|
dragHint.className = "lm-drag-hint";
|
|
strengthLabel.appendChild(dragHint);
|
|
|
|
header.appendChild(toggleContainer);
|
|
header.appendChild(strengthLabel);
|
|
container.appendChild(header);
|
|
|
|
// Initialize the header drag functionality
|
|
initHeaderDrag(header, widget, renderLoras);
|
|
|
|
// Track the total visible entries for height calculation
|
|
let totalVisibleEntries = lorasData.length;
|
|
|
|
// Render each lora entry
|
|
lorasData.forEach((loraData) => {
|
|
const { name, strength, clipStrength, active } = loraData;
|
|
|
|
// Determine expansion state using our helper function
|
|
const isExpanded = shouldShowClipEntry(loraData);
|
|
const strengthFocusEntry = createFocusEntry(name, "strength");
|
|
|
|
// Create the main LoRA entry
|
|
const loraEl = document.createElement("div");
|
|
loraEl.className = "lm-lora-entry";
|
|
|
|
// Store lora name, active state, and locked state in dataset
|
|
loraEl.dataset.loraName = name;
|
|
loraEl.dataset.active = active ? "true" : "false";
|
|
loraEl.dataset.locked = (loraData.locked || false) ? "true" : "false";
|
|
|
|
// Add click handler for selection
|
|
loraEl.addEventListener('click', (e) => {
|
|
// Skip if clicking on interactive elements
|
|
if (e.target.closest('.lm-lora-toggle') ||
|
|
e.target.closest('input') ||
|
|
e.target.closest('.lm-lora-arrow') ||
|
|
e.target.closest('.lm-lora-drag-handle') ||
|
|
e.target.closest('.lm-lora-lock-button') ||
|
|
e.target.closest('.lm-lora-expand-button')) {
|
|
return;
|
|
}
|
|
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
selectLora(name);
|
|
container.focus(); // Focus container for keyboard events
|
|
});
|
|
|
|
// Conditionally create drag handle OR lock button
|
|
let dragHandleOrLockButton;
|
|
|
|
if (isRandomizerNode) {
|
|
// For randomizer node, show lock button instead of drag handle
|
|
const isLocked = loraData.locked || false;
|
|
dragHandleOrLockButton = createLockButton(isLocked, (newLocked) => {
|
|
// Update this lora's locked state
|
|
const lorasData = parseLoraValue(widget.value);
|
|
const loraIndex = lorasData.findIndex(l => l.name === name);
|
|
|
|
if (loraIndex >= 0) {
|
|
lorasData[loraIndex].locked = newLocked;
|
|
const newValue = formatLoraValue(lorasData);
|
|
updateWidgetValue(newValue);
|
|
}
|
|
});
|
|
} else {
|
|
// For other nodes, show drag handle
|
|
dragHandleOrLockButton = createDragHandle();
|
|
// Initialize reorder drag functionality
|
|
initReorderDrag(dragHandleOrLockButton, name, widget, renderLoras);
|
|
}
|
|
|
|
// Create toggle for this lora
|
|
const toggle = createToggle(active, (newActive) => {
|
|
// Update this lora's active state
|
|
const lorasData = parseLoraValue(widget.value);
|
|
const loraIndex = lorasData.findIndex(l => l.name === name);
|
|
|
|
if (loraIndex >= 0) {
|
|
lorasData[loraIndex].active = newActive;
|
|
|
|
if (selectedLora === name) {
|
|
emitSelectionChange({
|
|
name,
|
|
active: newActive,
|
|
entry: { ...lorasData[loraIndex] },
|
|
});
|
|
}
|
|
|
|
const newValue = formatLoraValue(lorasData);
|
|
updateWidgetValue(newValue);
|
|
}
|
|
});
|
|
|
|
// Create expand button
|
|
const expandButton = createExpandButton(isExpanded, (shouldExpand) => {
|
|
// Toggle the clip entry expanded state
|
|
const lorasData = parseLoraValue(widget.value);
|
|
const loraIndex = lorasData.findIndex(l => l.name === name);
|
|
|
|
if (loraIndex >= 0) {
|
|
// Set the expansion state
|
|
lorasData[loraIndex].expanded = shouldExpand;
|
|
|
|
// If collapsing, set clipStrength = strength
|
|
if (!shouldExpand) {
|
|
lorasData[loraIndex].clipStrength = lorasData[loraIndex].strength;
|
|
}
|
|
|
|
// Update the widget value
|
|
updateWidgetValue(formatLoraValue(lorasData));
|
|
|
|
// Re-render to show/hide clip entry
|
|
renderLoras(widget.value, widget);
|
|
}
|
|
});
|
|
|
|
// Create name display
|
|
const nameEl = document.createElement("div");
|
|
nameEl.textContent = name;
|
|
nameEl.className = "lm-lora-name";
|
|
|
|
// Move preview tooltip events to nameEl instead of loraEl
|
|
let previewTimer = null; // Timer for delayed preview
|
|
|
|
const clearPreviewTimer = () => {
|
|
if (previewTimer) {
|
|
clearTimeout(previewTimer);
|
|
previewTimer = null;
|
|
}
|
|
};
|
|
|
|
nameEl.addEventListener('mouseenter', (e) => {
|
|
e.stopPropagation();
|
|
if (shouldSuppressPreview()) {
|
|
return;
|
|
}
|
|
previewTimer = setTimeout(async () => {
|
|
previewTimer = null;
|
|
if (shouldSuppressPreview()) {
|
|
return;
|
|
}
|
|
const rect = nameEl.getBoundingClientRect();
|
|
await previewTooltip.show(name, rect.right, rect.top);
|
|
}, 400); // 400ms delay
|
|
});
|
|
|
|
nameEl.addEventListener('mouseleave', (e) => {
|
|
e.stopPropagation();
|
|
clearPreviewTimer(); // Cancel if not triggered
|
|
previewTooltip.hide();
|
|
});
|
|
|
|
// Initialize drag functionality for strength adjustment
|
|
initDrag(loraEl, name, widget, false, previewTooltip, renderLoras, {
|
|
onDragStart: () => {
|
|
clearPreviewTimer();
|
|
markStrengthDragStart();
|
|
},
|
|
onDragEnd: () => {
|
|
clearPreviewTimer();
|
|
markStrengthDragEnd();
|
|
}
|
|
});
|
|
|
|
// Add context menu event
|
|
loraEl.addEventListener('contextmenu', (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
createContextMenu(e.clientX, e.clientY, name, widget, previewTooltip, renderLoras);
|
|
});
|
|
|
|
// Create strength control
|
|
const strengthControl = document.createElement("div");
|
|
strengthControl.className = "lm-lora-strength-control";
|
|
|
|
// Left arrow
|
|
const leftArrow = createArrowButton("left", () => {
|
|
// Decrease strength
|
|
const lorasData = parseLoraValue(widget.value);
|
|
const loraIndex = lorasData.findIndex(l => l.name === name);
|
|
|
|
if (loraIndex >= 0) {
|
|
lorasData[loraIndex].strength = (parseFloat(lorasData[loraIndex].strength) - getStrengthStepPreference()).toFixed(2);
|
|
// Sync clipStrength if collapsed
|
|
syncClipStrengthIfCollapsed(lorasData[loraIndex]);
|
|
|
|
const newValue = formatLoraValue(lorasData);
|
|
updateWidgetValue(newValue);
|
|
}
|
|
});
|
|
|
|
// Strength display
|
|
const strengthEl = document.createElement("input");
|
|
strengthEl.classList.add("lm-lora-strength-input");
|
|
strengthEl.type = "text";
|
|
strengthEl.value = typeof strength === 'number' ? strength.toFixed(2) : Number(strength).toFixed(2);
|
|
strengthEl.addEventListener('pointerdown', () => {
|
|
pendingFocusTarget = { name, type: "strength" };
|
|
});
|
|
|
|
// Handle focus
|
|
strengthEl.addEventListener('focus', () => {
|
|
pendingFocusTarget = null;
|
|
// Auto-select all content
|
|
strengthEl.select();
|
|
selectLora(name);
|
|
});
|
|
|
|
// Handle input changes
|
|
const commitStrengthValue = () => {
|
|
let parsedValue = parseFloat(strengthEl.value);
|
|
if (isNaN(parsedValue)) {
|
|
parsedValue = 1.0;
|
|
}
|
|
const normalizedValue = parsedValue.toFixed(2);
|
|
|
|
const currentLoras = parseLoraValue(widget.value);
|
|
const loraIndex = currentLoras.findIndex(l => l.name === name);
|
|
|
|
if (loraIndex >= 0) {
|
|
currentLoras[loraIndex].strength = normalizedValue;
|
|
// Sync clipStrength if collapsed
|
|
syncClipStrengthIfCollapsed(currentLoras[loraIndex]);
|
|
|
|
strengthEl.value = normalizedValue;
|
|
const newLorasValue = formatLoraValue(currentLoras);
|
|
updateWidgetValue(newLorasValue);
|
|
} else {
|
|
strengthEl.value = normalizedValue;
|
|
}
|
|
};
|
|
|
|
strengthEl.addEventListener('change', commitStrengthValue);
|
|
|
|
// Handle key events
|
|
strengthEl.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Enter') {
|
|
strengthEl.blur();
|
|
} else if (e.key === 'Tab') {
|
|
const moved = queueFocusAdjacentFrom(strengthFocusEntry, e.shiftKey ? -1 : 1);
|
|
commitStrengthValue();
|
|
if (moved) {
|
|
e.preventDefault();
|
|
}
|
|
}
|
|
});
|
|
|
|
// Right arrow
|
|
const rightArrow = createArrowButton("right", () => {
|
|
// Increase strength
|
|
const lorasData = parseLoraValue(widget.value);
|
|
const loraIndex = lorasData.findIndex(l => l.name === name);
|
|
|
|
if (loraIndex >= 0) {
|
|
lorasData[loraIndex].strength = (parseFloat(lorasData[loraIndex].strength) + getStrengthStepPreference()).toFixed(2);
|
|
// Sync clipStrength if collapsed
|
|
syncClipStrengthIfCollapsed(lorasData[loraIndex]);
|
|
|
|
const newValue = formatLoraValue(lorasData);
|
|
updateWidgetValue(newValue);
|
|
}
|
|
});
|
|
|
|
strengthControl.appendChild(leftArrow);
|
|
strengthControl.appendChild(strengthEl);
|
|
strengthControl.appendChild(rightArrow);
|
|
|
|
// Assemble entry
|
|
const leftSection = document.createElement("div");
|
|
leftSection.className = "lm-lora-entry-left";
|
|
|
|
leftSection.appendChild(dragHandleOrLockButton); // Add drag handle or lock button first
|
|
leftSection.appendChild(toggle);
|
|
leftSection.appendChild(expandButton); // Add expand button
|
|
leftSection.appendChild(nameEl);
|
|
|
|
loraEl.appendChild(leftSection);
|
|
loraEl.appendChild(strengthControl);
|
|
|
|
container.appendChild(loraEl);
|
|
|
|
// If expanded, show the clip entry
|
|
if (isExpanded) {
|
|
totalVisibleEntries++;
|
|
const clipEl = document.createElement("div");
|
|
clipEl.className = "lm-lora-clip-entry";
|
|
|
|
// Store the same lora name in clip entry dataset
|
|
clipEl.dataset.loraName = name;
|
|
clipEl.dataset.active = active ? "true" : "false";
|
|
|
|
// Create clip name display
|
|
const clipNameEl = document.createElement("div");
|
|
clipNameEl.textContent = "[clip] " + name;
|
|
clipNameEl.className = "lm-lora-name";
|
|
|
|
// Create clip strength control
|
|
const clipStrengthControl = document.createElement("div");
|
|
clipStrengthControl.className = "lm-lora-strength-control";
|
|
|
|
// Left arrow for clip
|
|
const clipLeftArrow = createArrowButton("left", () => {
|
|
// Decrease clip strength
|
|
const lorasData = parseLoraValue(widget.value);
|
|
const loraIndex = lorasData.findIndex(l => l.name === name);
|
|
|
|
if (loraIndex >= 0) {
|
|
lorasData[loraIndex].clipStrength = (parseFloat(lorasData[loraIndex].clipStrength) - getStrengthStepPreference()).toFixed(2);
|
|
|
|
const newValue = formatLoraValue(lorasData);
|
|
updateWidgetValue(newValue);
|
|
}
|
|
});
|
|
|
|
// Clip strength display
|
|
const clipStrengthEl = document.createElement("input");
|
|
clipStrengthEl.classList.add("lm-lora-strength-input", "lm-lora-clip-strength-input");
|
|
clipStrengthEl.type = "text";
|
|
clipStrengthEl.value = typeof clipStrength === 'number' ? clipStrength.toFixed(2) : Number(clipStrength).toFixed(2);
|
|
clipStrengthEl.addEventListener('pointerdown', () => {
|
|
pendingFocusTarget = { name, type: "clip" };
|
|
});
|
|
|
|
// Handle focus
|
|
clipStrengthEl.addEventListener('focus', () => {
|
|
pendingFocusTarget = null;
|
|
// Auto-select all content
|
|
clipStrengthEl.select();
|
|
selectLora(name);
|
|
});
|
|
|
|
// Handle input changes
|
|
const clipFocusEntry = createFocusEntry(name, "clip");
|
|
|
|
const commitClipStrengthValue = () => {
|
|
let parsedValue = parseFloat(clipStrengthEl.value);
|
|
if (isNaN(parsedValue)) {
|
|
parsedValue = 1.0;
|
|
}
|
|
const normalizedValue = parsedValue.toFixed(2);
|
|
|
|
const currentLoras = parseLoraValue(widget.value);
|
|
const loraIndex = currentLoras.findIndex(l => l.name === name);
|
|
|
|
if (loraIndex >= 0) {
|
|
currentLoras[loraIndex].clipStrength = normalizedValue;
|
|
clipStrengthEl.value = normalizedValue;
|
|
|
|
const newLorasValue = formatLoraValue(currentLoras);
|
|
updateWidgetValue(newLorasValue);
|
|
} else {
|
|
clipStrengthEl.value = normalizedValue;
|
|
}
|
|
};
|
|
|
|
clipStrengthEl.addEventListener('change', commitClipStrengthValue);
|
|
|
|
// Handle key events
|
|
clipStrengthEl.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Enter') {
|
|
clipStrengthEl.blur();
|
|
} else if (e.key === 'Tab') {
|
|
const moved = queueFocusAdjacentFrom(clipFocusEntry, e.shiftKey ? -1 : 1);
|
|
commitClipStrengthValue();
|
|
if (moved) {
|
|
e.preventDefault();
|
|
}
|
|
}
|
|
});
|
|
|
|
// Right arrow for clip
|
|
const clipRightArrow = createArrowButton("right", () => {
|
|
// Increase clip strength
|
|
const lorasData = parseLoraValue(widget.value);
|
|
const loraIndex = lorasData.findIndex(l => l.name === name);
|
|
|
|
if (loraIndex >= 0) {
|
|
lorasData[loraIndex].clipStrength = (parseFloat(lorasData[loraIndex].clipStrength) + getStrengthStepPreference()).toFixed(2);
|
|
|
|
const newValue = formatLoraValue(lorasData);
|
|
updateWidgetValue(newValue);
|
|
}
|
|
});
|
|
|
|
clipStrengthControl.appendChild(clipLeftArrow);
|
|
clipStrengthControl.appendChild(clipStrengthEl);
|
|
clipStrengthControl.appendChild(clipRightArrow);
|
|
|
|
// Assemble clip entry
|
|
const clipLeftSection = document.createElement("div");
|
|
clipLeftSection.className = "lm-lora-entry-left";
|
|
|
|
clipLeftSection.appendChild(clipNameEl);
|
|
|
|
clipEl.appendChild(clipLeftSection);
|
|
clipEl.appendChild(clipStrengthControl);
|
|
|
|
// Add drag functionality to clip entry
|
|
initDrag(clipEl, name, widget, true, previewTooltip, renderLoras, {
|
|
onDragStart: markStrengthDragStart,
|
|
onDragEnd: markStrengthDragEnd
|
|
});
|
|
|
|
container.appendChild(clipEl);
|
|
}
|
|
});
|
|
|
|
// Calculate height based on number of loras and fixed sizes
|
|
const calculatedHeight = CONTAINER_PADDING + HEADER_HEIGHT + (Math.min(totalVisibleEntries, 12) * LORA_ENTRY_HEIGHT);
|
|
updateWidgetHeight(container, calculatedHeight, defaultHeight, node);
|
|
|
|
// After all LoRA elements are created, apply selection state as the last step
|
|
// This ensures the selection state is not overwritten
|
|
container.querySelectorAll('.lm-lora-entry').forEach(entry => {
|
|
const entryLoraName = entry.dataset.loraName;
|
|
updateEntrySelection(entry, entryLoraName === selectedLora);
|
|
});
|
|
|
|
const selectionExists = selectedLora
|
|
? currentLorasData.some((lora) => lora.name === selectedLora)
|
|
: false;
|
|
|
|
if (selectedLora && !selectionExists) {
|
|
selectLora(null);
|
|
} else if (selectedLora) {
|
|
emitSelectionChange(buildSelectionPayload(selectedLora));
|
|
}
|
|
|
|
if (pendingFocusTarget) {
|
|
const focusTarget = pendingFocusTarget;
|
|
const safeName = escapeLoraName(focusTarget.name);
|
|
let selector = "";
|
|
|
|
if (focusTarget.type === "strength") {
|
|
selector = `.lm-lora-entry[data-lora-name="${safeName}"] .lm-lora-strength-input`;
|
|
} else if (focusTarget.type === "clip") {
|
|
selector = `.lm-lora-clip-entry[data-lora-name="${safeName}"] .lm-lora-clip-strength-input`;
|
|
}
|
|
|
|
if (selector) {
|
|
const targetInput = container.querySelector(selector);
|
|
if (targetInput) {
|
|
requestAnimationFrame(() => {
|
|
targetInput.focus();
|
|
if (typeof targetInput.select === "function") {
|
|
targetInput.select();
|
|
}
|
|
selectLora(focusTarget.name, { silent: true });
|
|
});
|
|
}
|
|
}
|
|
|
|
pendingFocusTarget = null;
|
|
}
|
|
};
|
|
|
|
// Store the value in a variable to avoid recursion
|
|
let widgetValue = defaultValue;
|
|
|
|
// Create widget with new DOM Widget API
|
|
const widget = node.addDOMWidget(name, "custom", container, {
|
|
getValue: function() {
|
|
return widgetValue;
|
|
},
|
|
setValue: function(v) {
|
|
// Remove duplicates by keeping the last occurrence of each lora name
|
|
const uniqueValue = (v || []).reduce((acc, lora) => {
|
|
// Remove any existing lora with the same name
|
|
const filtered = acc.filter(l => l.name !== lora.name);
|
|
// Add the current lora
|
|
return [...filtered, lora];
|
|
}, []);
|
|
|
|
// Apply existing clip strength values and transfer them to the new value
|
|
const updatedValue = uniqueValue.map(lora => {
|
|
// For new loras, default clip strength to model strength and expanded to false
|
|
// unless clipStrength is already different from strength
|
|
const clipStrength = lora.clipStrength || lora.strength;
|
|
return {
|
|
...lora,
|
|
clipStrength: clipStrength,
|
|
expanded: lora.hasOwnProperty('expanded') ?
|
|
lora.expanded :
|
|
Number(clipStrength) !== Number(lora.strength),
|
|
locked: lora.hasOwnProperty('locked') ? lora.locked : false // Initialize locked to false if not present
|
|
};
|
|
});
|
|
|
|
widgetValue = updatedValue;
|
|
renderLoras(widgetValue, widget);
|
|
},
|
|
hideOnZoom: true,
|
|
selectOn: ['click', 'focus']
|
|
});
|
|
|
|
widget.value = defaultValue;
|
|
|
|
widget.callback = callback;
|
|
|
|
widget.onRemove = () => {
|
|
container.remove();
|
|
previewTooltip.cleanup();
|
|
// Remove keyboard event listener
|
|
container.removeEventListener('keydown', handleKeyboardNavigation);
|
|
};
|
|
|
|
return { minWidth: 400, minHeight: defaultHeight, widget };
|
|
}
|