diff --git a/web/comfyui/lora_loader.js b/web/comfyui/lora_loader.js
index a5e40c41..958f4878 100644
--- a/web/comfyui/lora_loader.js
+++ b/web/comfyui/lora_loader.js
@@ -4,39 +4,11 @@ import {
LORA_PATTERN,
collectActiveLorasFromChain,
updateConnectedTriggerWords,
- chainCallback
+ chainCallback,
+ mergeLoras
} from "./utils.js";
import { addLorasWidget } from "./loras_widget.js";
-function mergeLoras(lorasText, lorasArr) {
- const result = [];
- let match;
-
- // Reset pattern index before using
- LORA_PATTERN.lastIndex = 0;
-
- // Parse text input and create initial entries
- while ((match = LORA_PATTERN.exec(lorasText)) !== null) {
- const name = match[1];
- const modelStrength = Number(match[2]);
- // Extract clip strength if provided, otherwise use model strength
- const clipStrength = match[3] ? Number(match[3]) : modelStrength;
-
- // Find if this lora exists in the array data
- const existingLora = lorasArr.find(l => l.name === name);
-
- result.push({
- name: name,
- // Use existing strength if available, otherwise use input strength
- strength: existingLora ? existingLora.strength : modelStrength,
- active: existingLora ? existingLora.active : true,
- clipStrength: existingLora ? existingLora.clipStrength : clipStrength,
- });
- }
-
- return result;
-}
-
app.registerExtension({
name: "LoraManager.LoraLoader",
diff --git a/web/comfyui/lora_stacker.js b/web/comfyui/lora_stacker.js
index 81d2338e..1309ec66 100644
--- a/web/comfyui/lora_stacker.js
+++ b/web/comfyui/lora_stacker.js
@@ -4,39 +4,11 @@ import {
getActiveLorasFromNode,
collectActiveLorasFromChain,
updateConnectedTriggerWords,
- chainCallback
+ chainCallback,
+ mergeLoras
} from "./utils.js";
import { addLorasWidget } from "./loras_widget.js";
-function mergeLoras(lorasText, lorasArr) {
- const result = [];
- let match;
-
- // Reset pattern index before using
- LORA_PATTERN.lastIndex = 0;
-
- // Parse text input and create initial entries
- while ((match = LORA_PATTERN.exec(lorasText)) !== null) {
- const name = match[1];
- const modelStrength = Number(match[2]);
- // Extract clip strength if provided, otherwise use model strength
- const clipStrength = match[3] ? Number(match[3]) : modelStrength;
-
- // Find if this lora exists in the array data
- const existingLora = lorasArr.find(l => l.name === name);
-
- result.push({
- name: name,
- // Use existing strength if available, otherwise use input strength
- strength: existingLora ? existingLora.strength : modelStrength,
- active: existingLora ? existingLora.active : true,
- clipStrength: existingLora ? existingLora.clipStrength : clipStrength,
- });
- }
-
- return result;
-}
-
app.registerExtension({
name: "LoraManager.LoraStacker",
diff --git a/web/comfyui/loras_widget.js b/web/comfyui/loras_widget.js
index 066e388b..8f317c34 100644
--- a/web/comfyui/loras_widget.js
+++ b/web/comfyui/loras_widget.js
@@ -1,5 +1,4 @@
-import { app } from "../../scripts/app.js";
-import { createToggle, createArrowButton, PreviewTooltip } from "./loras_widget_components.js";
+import { createToggle, createArrowButton, PreviewTooltip, createDragHandle, updateEntrySelection } from "./loras_widget_components.js";
import {
parseLoraValue,
formatLoraValue,
@@ -11,7 +10,7 @@ import {
CONTAINER_PADDING,
EMPTY_CONTAINER_HEIGHT
} from "./loras_widget_utils.js";
-import { initDrag, createContextMenu, initHeaderDrag } from "./loras_widget_events.js";
+import { initDrag, createContextMenu, initHeaderDrag, initReorderDrag, handleKeyboardNavigation } from "./loras_widget_events.js";
export function addLorasWidget(node, name, opts, callback) {
// Create container for loras
@@ -42,6 +41,30 @@ export function addLorasWidget(node, name, opts, callback) {
// Create preview tooltip instance
const previewTooltip = new PreviewTooltip();
+ // Selection state - only one LoRA can be selected at a time
+ let selectedLora = null;
+
+ // Function to select a LoRA
+ const selectLora = (loraName) => {
+ selectedLora = loraName;
+ // Update visual feedback for all entries
+ container.querySelectorAll('.comfy-lora-entry').forEach(entry => {
+ const entryLoraName = entry.dataset.loraName;
+ updateEntrySelection(entry, entryLoraName === selectedLora);
+ });
+ };
+
+ // 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;
+ container.style.outline = 'none';
+
// Function to render loras from data
const renderLoras = (value, widget) => {
// Clear existing content
@@ -185,6 +208,26 @@ export function addLorasWidget(node, name, opts, callback) {
marginBottom: "4px",
});
+ // Store lora name and active state in dataset for selection
+ loraEl.dataset.loraName = name;
+ loraEl.dataset.active = active;
+
+ // Add click handler for selection
+ loraEl.addEventListener('click', (e) => {
+ // Skip if clicking on interactive elements
+ if (e.target.closest('.comfy-lora-toggle') ||
+ e.target.closest('input') ||
+ e.target.closest('.comfy-lora-arrow') ||
+ e.target.closest('.comfy-lora-drag-handle')) {
+ return;
+ }
+
+ e.preventDefault();
+ e.stopPropagation();
+ selectLora(name);
+ container.focus(); // Focus container for keyboard events
+ });
+
// Add double-click handler to toggle clip entry
loraEl.addEventListener('dblclick', (e) => {
// Skip if clicking on toggle or strength control areas
@@ -220,6 +263,12 @@ export function addLorasWidget(node, name, opts, callback) {
}
});
+ // Create drag handle for reordering
+ const dragHandle = createDragHandle();
+
+ // Initialize reorder drag functionality
+ initReorderDrag(dragHandle, name, widget, renderLoras);
+
// Create toggle for this lora
const toggle = createToggle(active, (newActive) => {
// Update this lora's active state
@@ -416,6 +465,7 @@ export function addLorasWidget(node, name, opts, callback) {
minWidth: "0", // Allow shrinking
});
+ leftSection.appendChild(dragHandle); // Add drag handle first
leftSection.appendChild(toggle);
leftSection.appendChild(nameEl);
@@ -424,6 +474,9 @@ export function addLorasWidget(node, name, opts, callback) {
container.appendChild(loraEl);
+ // Update selection state
+ updateEntrySelection(loraEl, name === selectedLora);
+
// If expanded, show the clip entry
if (isExpanded) {
totalVisibleEntries++;
@@ -444,6 +497,10 @@ export function addLorasWidget(node, name, opts, callback) {
marginTop: "-2px"
});
+ // Store the same lora name in clip entry dataset
+ clipEl.dataset.loraName = name;
+ clipEl.dataset.active = active;
+
// Create clip name display
const clipNameEl = document.createElement("div");
clipNameEl.textContent = "[clip] " + name;
@@ -601,7 +658,7 @@ export function addLorasWidget(node, name, opts, callback) {
});
// Calculate height based on number of loras and fixed sizes
- const calculatedHeight = CONTAINER_PADDING + HEADER_HEIGHT + (Math.min(totalVisibleEntries, 10) * LORA_ENTRY_HEIGHT);
+ const calculatedHeight = CONTAINER_PADDING + HEADER_HEIGHT + (Math.min(totalVisibleEntries, 12) * LORA_ENTRY_HEIGHT);
updateWidgetHeight(container, calculatedHeight, defaultHeight, node);
};
@@ -685,6 +742,8 @@ export function addLorasWidget(node, name, opts, callback) {
widget.onRemove = () => {
container.remove();
previewTooltip.cleanup();
+ // Remove keyboard event listener
+ container.removeEventListener('keydown', handleKeyboardNavigation);
};
return { minWidth: 400, minHeight: defaultHeight, widget };
diff --git a/web/comfyui/loras_widget_components.js b/web/comfyui/loras_widget_components.js
index 4a4448e0..f2a18bf7 100644
--- a/web/comfyui/loras_widget_components.js
+++ b/web/comfyui/loras_widget_components.js
@@ -78,6 +78,87 @@ export function createArrowButton(direction, onClick) {
return button;
}
+// Function to create drag handle
+export function createDragHandle() {
+ const handle = document.createElement("div");
+ handle.className = "comfy-lora-drag-handle";
+ handle.innerHTML = "≡";
+ handle.title = "Drag to reorder LoRA";
+
+ Object.assign(handle.style, {
+ width: "16px",
+ height: "16px",
+ display: "flex",
+ alignItems: "center",
+ justifyContent: "center",
+ cursor: "grab",
+ userSelect: "none",
+ fontSize: "14px",
+ color: "rgba(226, 232, 240, 0.6)",
+ transition: "all 0.2s ease",
+ marginRight: "8px",
+ flexShrink: "0"
+ });
+
+ // Add hover effect
+ handle.onmouseenter = () => {
+ handle.style.color = "rgba(226, 232, 240, 0.9)";
+ handle.style.transform = "scale(1.1)";
+ };
+
+ handle.onmouseleave = () => {
+ handle.style.color = "rgba(226, 232, 240, 0.6)";
+ handle.style.transform = "scale(1)";
+ };
+
+ // Change cursor when dragging
+ handle.onmousedown = () => {
+ handle.style.cursor = "grabbing";
+ };
+
+ return handle;
+}
+
+// Function to create drop indicator
+export function createDropIndicator() {
+ const indicator = document.createElement("div");
+ indicator.className = "comfy-lora-drop-indicator";
+
+ Object.assign(indicator.style, {
+ position: "absolute",
+ left: "0",
+ right: "0",
+ height: "3px",
+ backgroundColor: "rgba(66, 153, 225, 0.9)",
+ borderRadius: "2px",
+ opacity: "0",
+ transition: "opacity 0.2s ease",
+ boxShadow: "0 0 6px rgba(66, 153, 225, 0.8)",
+ zIndex: "10",
+ pointerEvents: "none"
+ });
+
+ return indicator;
+}
+
+// Function to update entry selection state
+export function updateEntrySelection(entryEl, isSelected) {
+ const baseColor = entryEl.dataset.active === 'true' ?
+ "rgba(45, 55, 72, 0.7)" : "rgba(35, 40, 50, 0.5)";
+ const selectedColor = entryEl.dataset.active === 'true' ?
+ "rgba(66, 153, 225, 0.3)" : "rgba(66, 153, 225, 0.2)";
+
+ if (isSelected) {
+ entryEl.style.backgroundColor = selectedColor;
+ entryEl.style.border = "1px solid rgba(66, 153, 225, 0.6)";
+ entryEl.style.boxShadow = "0 0 0 1px rgba(66, 153, 225, 0.3)";
+ } else {
+ entryEl.style.backgroundColor = baseColor;
+ entryEl.style.border = "1px solid transparent";
+ entryEl.style.boxShadow = "none";
+ }
+}
+
// Function to create menu item
export function createMenuItem(text, icon, onClick) {
const menuItem = document.createElement('div');
diff --git a/web/comfyui/loras_widget_events.js b/web/comfyui/loras_widget_events.js
index 0c1e56b4..e03fa08f 100644
--- a/web/comfyui/loras_widget_events.js
+++ b/web/comfyui/loras_widget_events.js
@@ -1,6 +1,7 @@
import { api } from "../../scripts/api.js";
-import { createMenuItem } from "./loras_widget_components.js";
-import { parseLoraValue, formatLoraValue, syncClipStrengthIfCollapsed, saveRecipeDirectly, copyToClipboard, showToast } from "./loras_widget_utils.js";
+import { app } from "../../scripts/app.js";
+import { createMenuItem, createDropIndicator } from "./loras_widget_components.js";
+import { parseLoraValue, formatLoraValue, syncClipStrengthIfCollapsed, saveRecipeDirectly, copyToClipboard, showToast, moveLoraByDirection, getDropTargetIndex } from "./loras_widget_utils.js";
// Function to handle strength adjustment via dragging
export function handleStrengthDrag(name, initialStrength, initialX, event, widget, isClipStrength = false) {
@@ -227,6 +228,223 @@ export function initHeaderDrag(headerEl, widget, renderFunction) {
});
}
+// Function to initialize drag-and-drop for reordering
+export function initReorderDrag(dragHandle, loraName, widget, renderFunction) {
+ let isDragging = false;
+ let draggedElement = null;
+ let dropIndicator = null;
+ let container = null;
+ let scale = 1;
+
+ dragHandle.addEventListener('mousedown', (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+
+ isDragging = true;
+ draggedElement = dragHandle.closest('.comfy-lora-entry');
+ container = draggedElement.parentElement;
+
+ // Add dragging class and visual feedback
+ draggedElement.classList.add('comfy-lora-dragging');
+ draggedElement.style.opacity = '0.5';
+ draggedElement.style.transform = 'scale(0.98)';
+
+ // Create single drop indicator with absolute positioning
+ dropIndicator = createDropIndicator();
+
+ // Make container relatively positioned for absolute indicator
+ const originalPosition = container.style.position;
+ container.style.position = 'relative';
+ container.appendChild(dropIndicator);
+
+ // Store original position for cleanup
+ container._originalPosition = originalPosition;
+
+ // Add global cursor style
+ document.body.style.cursor = 'grabbing';
+
+ // Store workflow scale for accurate positioning
+ scale = app.canvas.ds.scale;
+ });
+
+ document.addEventListener('mousemove', (e) => {
+ if (!isDragging || !draggedElement || !dropIndicator) return;
+
+ const targetIndex = getDropTargetIndex(container, e.clientY);
+ const entries = container.querySelectorAll('.comfy-lora-entry, .comfy-lora-clip-entry');
+
+ if (targetIndex === 0) {
+ // Show at top
+ const firstEntry = entries[0];
+ if (firstEntry) {
+ const rect = firstEntry.getBoundingClientRect();
+ const containerRect = container.getBoundingClientRect();
+ dropIndicator.style.top = `${(rect.top - containerRect.top - 2) / scale}px`;
+ dropIndicator.style.opacity = '1';
+ }
+ } else if (targetIndex < entries.length) {
+ // Show between entries
+ const targetEntry = entries[targetIndex];
+ if (targetEntry) {
+ const rect = targetEntry.getBoundingClientRect();
+ const containerRect = container.getBoundingClientRect();
+ dropIndicator.style.top = `${(rect.top - containerRect.top - 2) / scale}px`;
+ dropIndicator.style.opacity = '1';
+ }
+ } else {
+ // Show at bottom
+ const lastEntry = entries[entries.length - 1];
+ if (lastEntry) {
+ const rect = lastEntry.getBoundingClientRect();
+ const containerRect = container.getBoundingClientRect();
+ dropIndicator.style.top = `${(rect.bottom - containerRect.top + 2) / scale}px`;
+ dropIndicator.style.opacity = '1';
+ }
+ }
+ });
+
+ document.addEventListener('mouseup', (e) => {
+ if (!isDragging || !draggedElement) return;
+
+ const targetIndex = getDropTargetIndex(container, e.clientY);
+
+ // Get current LoRA data
+ const lorasData = parseLoraValue(widget.value);
+ const currentIndex = lorasData.findIndex(l => l.name === loraName);
+
+ if (currentIndex !== -1 && currentIndex !== targetIndex) {
+ // Calculate actual target index (excluding clip entries from count)
+ const loraEntries = container.querySelectorAll('.comfy-lora-entry');
+ let actualTargetIndex = targetIndex;
+
+ // Adjust target index if it's beyond the number of actual LoRA entries
+ if (actualTargetIndex > loraEntries.length) {
+ actualTargetIndex = loraEntries.length;
+ }
+
+ // Move the LoRA
+ const newLoras = [...lorasData];
+ const [moved] = newLoras.splice(currentIndex, 1);
+ newLoras.splice(actualTargetIndex > currentIndex ? actualTargetIndex - 1 : actualTargetIndex, 0, moved);
+
+ widget.value = formatLoraValue(newLoras);
+
+ if (widget.callback) {
+ widget.callback(widget.value);
+ }
+
+ // Re-render
+ if (renderFunction) {
+ renderFunction(widget.value, widget);
+ }
+ }
+
+ // Cleanup
+ isDragging = false;
+ if (draggedElement) {
+ draggedElement.classList.remove('comfy-lora-dragging');
+ draggedElement.style.opacity = '';
+ draggedElement.style.transform = '';
+ draggedElement = null;
+ }
+
+ if (dropIndicator && container) {
+ container.removeChild(dropIndicator);
+ // Restore original position
+ container.style.position = container._originalPosition || '';
+ delete container._originalPosition;
+ dropIndicator = null;
+ }
+
+ // Reset cursor
+ document.body.style.cursor = '';
+ container = null;
+ });
+}
+
+// Function to handle keyboard navigation
+export function handleKeyboardNavigation(event, selectedLora, widget, renderFunction, selectLora) {
+ if (!selectedLora) return false;
+
+ const lorasData = parseLoraValue(widget.value);
+ let handled = false;
+
+ // Check for Ctrl/Cmd modifier for reordering
+ if (event.ctrlKey || event.metaKey) {
+ switch (event.key) {
+ case 'ArrowUp':
+ event.preventDefault();
+ const newLorasUp = moveLoraByDirection(lorasData, selectedLora, 'up');
+ widget.value = formatLoraValue(newLorasUp);
+ if (widget.callback) widget.callback(widget.value);
+ if (renderFunction) renderFunction(widget.value, widget);
+ handled = true;
+ break;
+
+ case 'ArrowDown':
+ event.preventDefault();
+ const newLorasDown = moveLoraByDirection(lorasData, selectedLora, 'down');
+ widget.value = formatLoraValue(newLorasDown);
+ if (widget.callback) widget.callback(widget.value);
+ if (renderFunction) renderFunction(widget.value, widget);
+ handled = true;
+ break;
+
+ case 'Home':
+ event.preventDefault();
+ const newLorasTop = moveLoraByDirection(lorasData, selectedLora, 'top');
+ widget.value = formatLoraValue(newLorasTop);
+ if (widget.callback) widget.callback(widget.value);
+ if (renderFunction) renderFunction(widget.value, widget);
+ handled = true;
+ break;
+
+ case 'End':
+ event.preventDefault();
+ const newLorasBottom = moveLoraByDirection(lorasData, selectedLora, 'bottom');
+ widget.value = formatLoraValue(newLorasBottom);
+ if (widget.callback) widget.callback(widget.value);
+ if (renderFunction) renderFunction(widget.value, widget);
+ handled = true;
+ break;
+ }
+ } else {
+ // Normal navigation without Ctrl/Cmd
+ switch (event.key) {
+ case 'ArrowUp':
+ event.preventDefault();
+ const currentIndex = lorasData.findIndex(l => l.name === selectedLora);
+ if (currentIndex > 0) {
+ selectLora(lorasData[currentIndex - 1].name);
+ }
+ handled = true;
+ break;
+
+ case 'ArrowDown':
+ event.preventDefault();
+ const currentIndexDown = lorasData.findIndex(l => l.name === selectedLora);
+ if (currentIndexDown < lorasData.length - 1) {
+ selectLora(lorasData[currentIndexDown + 1].name);
+ }
+ handled = true;
+ break;
+
+ case 'Delete':
+ case 'Backspace':
+ event.preventDefault();
+ const filtered = lorasData.filter(l => l.name !== selectedLora);
+ widget.value = formatLoraValue(filtered);
+ if (widget.callback) widget.callback(widget.value);
+ if (renderFunction) renderFunction(widget.value, widget);
+ selectLora(null); // Clear selection
+ handled = true;
+ break;
+ }
+ }
+
+ return handled;
+}
+
// Function to create context menu
export function createContextMenu(x, y, loraName, widget, previewTooltip, renderFunction) {
// Hide preview tooltip first
@@ -398,6 +616,94 @@ export function createContextMenu(x, y, loraName, widget, previewTooltip, render
}
);
+ // Move Up option with arrow up icon
+ const moveUpOption = createMenuItem(
+ 'Move Up',
+ '',
+ () => {
+ menu.remove();
+ document.removeEventListener('click', closeMenu);
+
+ const lorasData = parseLoraValue(widget.value);
+ const newLoras = moveLoraByDirection(lorasData, loraName, 'up');
+ widget.value = formatLoraValue(newLoras);
+
+ if (widget.callback) {
+ widget.callback(widget.value);
+ }
+
+ if (renderFunction) {
+ renderFunction(widget.value, widget);
+ }
+ }
+ );
+
+ // Move Down option with arrow down icon
+ const moveDownOption = createMenuItem(
+ 'Move Down',
+ '',
+ () => {
+ menu.remove();
+ document.removeEventListener('click', closeMenu);
+
+ const lorasData = parseLoraValue(widget.value);
+ const newLoras = moveLoraByDirection(lorasData, loraName, 'down');
+ widget.value = formatLoraValue(newLoras);
+
+ if (widget.callback) {
+ widget.callback(widget.value);
+ }
+
+ if (renderFunction) {
+ renderFunction(widget.value, widget);
+ }
+ }
+ );
+
+ // Move to Top option with chevrons up icon
+ const moveTopOption = createMenuItem(
+ 'Move to Top',
+ '',
+ () => {
+ menu.remove();
+ document.removeEventListener('click', closeMenu);
+
+ const lorasData = parseLoraValue(widget.value);
+ const newLoras = moveLoraByDirection(lorasData, loraName, 'top');
+ widget.value = formatLoraValue(newLoras);
+
+ if (widget.callback) {
+ widget.callback(widget.value);
+ }
+
+ if (renderFunction) {
+ renderFunction(widget.value, widget);
+ }
+ }
+ );
+
+ // Move to Bottom option with chevrons down icon
+ const moveBottomOption = createMenuItem(
+ 'Move to Bottom',
+ '',
+ () => {
+ menu.remove();
+ document.removeEventListener('click', closeMenu);
+
+ const lorasData = parseLoraValue(widget.value);
+ const newLoras = moveLoraByDirection(lorasData, loraName, 'bottom');
+ widget.value = formatLoraValue(newLoras);
+
+ if (widget.callback) {
+ widget.callback(widget.value);
+ }
+
+ if (renderFunction) {
+ renderFunction(widget.value, widget);
+ }
+ }
+ );
+
// Add separator
const separator1 = document.createElement('div');
Object.assign(separator1.style, {
@@ -412,9 +718,21 @@ export function createContextMenu(x, y, loraName, widget, previewTooltip, render
borderTop: '1px solid rgba(255, 255, 255, 0.1)',
});
+ // Add separator for order options
+ const orderSeparator = document.createElement('div');
+ Object.assign(orderSeparator.style, {
+ margin: '4px 0',
+ borderTop: '1px solid rgba(255, 255, 255, 0.1)',
+ });
+
menu.appendChild(viewOnCivitaiOption);
menu.appendChild(deleteOption);
menu.appendChild(separator1);
+ menu.appendChild(moveUpOption);
+ menu.appendChild(moveDownOption);
+ menu.appendChild(moveTopOption);
+ menu.appendChild(moveBottomOption);
+ menu.appendChild(orderSeparator);
menu.appendChild(copyNotesOption);
menu.appendChild(copyTriggerWordsOption);
menu.appendChild(separator2);
diff --git a/web/comfyui/loras_widget_utils.js b/web/comfyui/loras_widget_utils.js
index d5dc30df..1b4193b3 100644
--- a/web/comfyui/loras_widget_utils.js
+++ b/web/comfyui/loras_widget_utils.js
@@ -3,7 +3,7 @@ import { app } from "../../scripts/app.js";
// Fixed sizes for component calculations
export const LORA_ENTRY_HEIGHT = 40; // Height of a single lora entry
export const CLIP_ENTRY_HEIGHT = 40; // Height of a clip entry
-export const HEADER_HEIGHT = 40; // Height of the header section
+export const HEADER_HEIGHT = 32; // Height of the header section
export const CONTAINER_PADDING = 12; // Top and bottom padding
export const EMPTY_CONTAINER_HEIGHT = 100; // Height when no loras are present
@@ -164,3 +164,71 @@ export function showToast(message, type = 'info') {
}
}
}
+
+/**
+ * Move a LoRA to a new position in the array
+ * @param {Array} loras - Array of LoRA objects
+ * @param {number} fromIndex - Current index of the LoRA
+ * @param {number} toIndex - Target index for the LoRA
+ * @returns {Array} - New array with LoRA moved
+ */
+export function moveLoraInArray(loras, fromIndex, toIndex) {
+ const newLoras = [...loras];
+ const [removed] = newLoras.splice(fromIndex, 1);
+ newLoras.splice(toIndex, 0, removed);
+ return newLoras;
+}
+
+/**
+ * Move a LoRA by name to a specific position
+ * @param {Array} loras - Array of LoRA objects
+ * @param {string} loraName - Name of the LoRA to move
+ * @param {string} direction - 'up', 'down', 'top', 'bottom'
+ * @returns {Array} - New array with LoRA moved
+ */
+export function moveLoraByDirection(loras, loraName, direction) {
+ const currentIndex = loras.findIndex(l => l.name === loraName);
+ if (currentIndex === -1) return loras;
+
+ let newIndex;
+ switch (direction) {
+ case 'up':
+ newIndex = Math.max(0, currentIndex - 1);
+ break;
+ case 'down':
+ newIndex = Math.min(loras.length - 1, currentIndex + 1);
+ break;
+ case 'top':
+ newIndex = 0;
+ break;
+ case 'bottom':
+ newIndex = loras.length - 1;
+ break;
+ default:
+ return loras;
+ }
+
+ if (newIndex === currentIndex) return loras;
+ return moveLoraInArray(loras, currentIndex, newIndex);
+}
+
+/**
+ * Get the drop target index based on mouse position
+ * @param {HTMLElement} container - The container element
+ * @param {number} clientY - Mouse Y position
+ * @returns {number} - Target index for dropping
+ */
+export function getDropTargetIndex(container, clientY) {
+ const entries = container.querySelectorAll('.comfy-lora-entry');
+ let targetIndex = entries.length;
+
+ for (let i = 0; i < entries.length; i++) {
+ const rect = entries[i].getBoundingClientRect();
+ if (clientY < rect.top + rect.height / 2) {
+ targetIndex = i;
+ break;
+ }
+ }
+
+ return targetIndex;
+}
diff --git a/web/comfyui/utils.js b/web/comfyui/utils.js
index 5acf395a..8d34ea5d 100644
--- a/web/comfyui/utils.js
+++ b/web/comfyui/utils.js
@@ -183,4 +183,47 @@ export function updateConnectedTriggerWords(node, loraNames) {
})
}).catch(err => console.error("Error fetching trigger words:", err));
}
+}
+
+export function mergeLoras(lorasText, lorasArr) {
+ // Parse lorasText into a map: name -> {strength, clipStrength}
+ const parsedLoras = {};
+ let match;
+ LORA_PATTERN.lastIndex = 0;
+ while ((match = LORA_PATTERN.exec(lorasText)) !== null) {
+ const name = match[1];
+ const modelStrength = Number(match[2]);
+ const clipStrength = match[3] ? Number(match[3]) : modelStrength;
+ parsedLoras[name] = { strength: modelStrength, clipStrength };
+ }
+
+ // Build result array in the order of lorasArr
+ const result = [];
+ const usedNames = new Set();
+
+ for (const lora of lorasArr) {
+ if (parsedLoras[lora.name]) {
+ result.push({
+ name: lora.name,
+ strength: lora.strength !== undefined ? lora.strength : parsedLoras[lora.name].strength,
+ active: lora.active !== undefined ? lora.active : true,
+ clipStrength: lora.clipStrength !== undefined ? lora.clipStrength : parsedLoras[lora.name].clipStrength,
+ });
+ usedNames.add(lora.name);
+ }
+ }
+
+ // Add any new loras from lorasText that are not in lorasArr, in their text order
+ for (const name in parsedLoras) {
+ if (!usedNames.has(name)) {
+ result.push({
+ name,
+ strength: parsedLoras[name].strength,
+ active: true,
+ clipStrength: parsedLoras[name].clipStrength,
+ });
+ }
+ }
+
+ return result;
}
\ No newline at end of file
diff --git a/web/comfyui/wanvideo_lora_select.js b/web/comfyui/wanvideo_lora_select.js
index 144530de..9712d9cf 100644
--- a/web/comfyui/wanvideo_lora_select.js
+++ b/web/comfyui/wanvideo_lora_select.js
@@ -2,41 +2,12 @@ import { app } from "../../scripts/app.js";
import {
LORA_PATTERN,
getActiveLorasFromNode,
- collectActiveLorasFromChain,
updateConnectedTriggerWords,
- chainCallback
+ chainCallback,
+ mergeLoras
} from "./utils.js";
import { addLorasWidget } from "./loras_widget.js";
-function mergeLoras(lorasText, lorasArr) {
- const result = [];
- let match;
-
- // Reset pattern index before using
- LORA_PATTERN.lastIndex = 0;
-
- // Parse text input and create initial entries
- while ((match = LORA_PATTERN.exec(lorasText)) !== null) {
- const name = match[1];
- const modelStrength = Number(match[2]);
- // Extract clip strength if provided, otherwise use model strength
- const clipStrength = match[3] ? Number(match[3]) : modelStrength;
-
- // Find if this lora exists in the array data
- const existingLora = lorasArr.find(l => l.name === name);
-
- result.push({
- name: name,
- // Use existing strength if available, otherwise use input strength
- strength: existingLora ? existingLora.strength : modelStrength,
- active: existingLora ? existingLora.active : true,
- clipStrength: existingLora ? existingLora.clipStrength : clipStrength,
- });
- }
-
- return result;
-}
-
app.registerExtension({
name: "LoraManager.WanVideoLoraSelect",