mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 13:12:12 -03:00
Add button condition checks in initDrag and initHeaderDrag functions to ensure only left mouse button (button 0) triggers drag interactions. This prevents conflicts with middle button canvas dragging and right button context menu actions, improving user experience and interaction clarity.
961 lines
32 KiB
JavaScript
961 lines
32 KiB
JavaScript
import { api } from "../../scripts/api.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, updateWidget = true) {
|
|
// Calculate drag sensitivity (how much the strength changes per pixel)
|
|
// Using 0.01 per 10 pixels of movement
|
|
const sensitivity = 0.001;
|
|
|
|
// Get the current mouse position
|
|
const currentX = event.clientX;
|
|
|
|
// Calculate the distance moved
|
|
const deltaX = currentX - initialX;
|
|
|
|
// Calculate the new strength value based on movement
|
|
// Moving right increases, moving left decreases
|
|
let newStrength = Number(initialStrength) + (deltaX * sensitivity);
|
|
|
|
// Limit the strength to reasonable bounds (now between -10 and 10)
|
|
newStrength = Math.max(-10, Math.min(10, newStrength));
|
|
newStrength = Number(newStrength.toFixed(2));
|
|
|
|
// Update the lora data
|
|
const lorasData = parseLoraValue(widget.value);
|
|
const loraIndex = lorasData.findIndex(l => l.name === name);
|
|
|
|
if (loraIndex >= 0) {
|
|
// Update the appropriate strength property based on isClipStrength flag
|
|
if (isClipStrength) {
|
|
lorasData[loraIndex].clipStrength = newStrength;
|
|
} else {
|
|
lorasData[loraIndex].strength = newStrength;
|
|
// Sync clipStrength if collapsed
|
|
syncClipStrengthIfCollapsed(lorasData[loraIndex]);
|
|
}
|
|
|
|
// Update the widget value only if updateWidget flag is true
|
|
// This allows us to update inputs directly during drag without triggering re-render
|
|
if (updateWidget) {
|
|
widget.value = formatLoraValue(lorasData);
|
|
}
|
|
|
|
// Force re-render via callback only if updateWidget is true
|
|
if (updateWidget && widget.callback) {
|
|
widget.callback(widget.value);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Function to handle proportional strength adjustment for all LoRAs via header dragging
|
|
export function handleAllStrengthsDrag(initialStrengths, initialX, event, widget, updateWidget = true) {
|
|
// Define sensitivity (less sensitive than individual adjustment)
|
|
const sensitivity = 0.0005;
|
|
|
|
// Get current mouse position
|
|
const currentX = event.clientX;
|
|
|
|
// Calculate the distance moved
|
|
const deltaX = currentX - initialX;
|
|
|
|
// Calculate adjustment factor (1.0 means no change, >1.0 means increase, <1.0 means decrease)
|
|
// For positive deltaX, we want to increase strengths, for negative we want to decrease
|
|
const adjustmentFactor = 1.0 + (deltaX * sensitivity);
|
|
|
|
// Ensure adjustment factor is reasonable (prevent extreme changes)
|
|
const limitedFactor = Math.max(0.01, Math.min(3.0, adjustmentFactor));
|
|
|
|
// Get current loras data
|
|
const lorasData = parseLoraValue(widget.value);
|
|
|
|
// Apply the adjustment factor to each LoRA's strengths
|
|
lorasData.forEach((loraData, index) => {
|
|
// Get initial strengths for this LoRA
|
|
const initialModelStrength = initialStrengths[index].modelStrength;
|
|
const initialClipStrength = initialStrengths[index].clipStrength;
|
|
|
|
// Apply the adjustment factor to both strengths
|
|
let newModelStrength = (initialModelStrength * limitedFactor).toFixed(2);
|
|
let newClipStrength = (initialClipStrength * limitedFactor).toFixed(2);
|
|
|
|
// Limit the values to reasonable bounds (-10 to 10)
|
|
newModelStrength = Math.max(-10, Math.min(10, newModelStrength));
|
|
newClipStrength = Math.max(-10, Math.min(10, newClipStrength));
|
|
|
|
// Update strengths
|
|
lorasData[index].strength = Number(newModelStrength);
|
|
lorasData[index].clipStrength = Number(newClipStrength);
|
|
});
|
|
|
|
// Update widget value only if updateWidget flag is true
|
|
if (updateWidget) {
|
|
widget.value = formatLoraValue(lorasData);
|
|
}
|
|
|
|
// Force re-render via callback only if updateWidget is true
|
|
if (updateWidget && widget.callback) {
|
|
widget.callback(widget.value);
|
|
}
|
|
}
|
|
|
|
// Function to initialize drag operation
|
|
export function initDrag(
|
|
dragEl,
|
|
name,
|
|
widget,
|
|
isClipStrength = false,
|
|
previewTooltip,
|
|
renderFunction,
|
|
dragCallbacks = {}
|
|
) {
|
|
let isDragging = false;
|
|
let activePointerId = null;
|
|
let initialX = 0;
|
|
let initialStrength = 0;
|
|
let currentDragElement = null;
|
|
let hasMoved = false;
|
|
const { onDragStart, onDragEnd } = dragCallbacks;
|
|
|
|
// Create a drag handler using pointer events for Vue DOM render mode compatibility
|
|
dragEl.addEventListener('pointerdown', (e) => {
|
|
// Skip if clicking on toggle or strength control areas
|
|
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;
|
|
}
|
|
|
|
// Only handle left mouse button (allow middle button for canvas drag, right button for context menu)
|
|
if (e.button !== 0) {
|
|
return;
|
|
}
|
|
|
|
// Store initial values
|
|
const lorasData = parseLoraValue(widget.value);
|
|
const loraData = lorasData.find(l => l.name === name);
|
|
|
|
if (!loraData) return;
|
|
|
|
initialX = e.clientX;
|
|
initialStrength = isClipStrength ? loraData.clipStrength : loraData.strength;
|
|
isDragging = true;
|
|
hasMoved = false;
|
|
activePointerId = e.pointerId;
|
|
currentDragElement = e.currentTarget;
|
|
|
|
// Capture pointer to receive all subsequent events regardless of stopPropagation
|
|
const target = e.currentTarget;
|
|
target.setPointerCapture(e.pointerId);
|
|
|
|
// Prevent text selection
|
|
e.preventDefault();
|
|
});
|
|
|
|
dragEl.addEventListener('pointermove', (e) => {
|
|
if (!isDragging) return;
|
|
|
|
// Track if pointer moved significantly (more than 3 pixels)
|
|
if (Math.abs(e.clientX - initialX) > 3) {
|
|
hasMoved = true;
|
|
}
|
|
|
|
// Only stop propagation if we've started dragging (moved beyond threshold)
|
|
if (hasMoved) {
|
|
e.stopPropagation();
|
|
}
|
|
|
|
// Only process drag if we've moved beyond threshold
|
|
if (!hasMoved) return;
|
|
|
|
// Add class to body to enforce cursor style globally only after drag starts
|
|
document.body.classList.add('lm-lora-strength-dragging');
|
|
|
|
if (typeof onDragStart === 'function') {
|
|
onDragStart();
|
|
}
|
|
|
|
// Call the strength adjustment function without updating widget.value during drag
|
|
handleStrengthDrag(name, initialStrength, initialX, e, widget, isClipStrength, false);
|
|
|
|
// Update strength input directly instead of re-rendering to avoid losing event listeners
|
|
const strengthInput = currentDragElement.querySelector('.lm-lora-strength-input');
|
|
if (strengthInput) {
|
|
const lorasData = parseLoraValue(widget.value);
|
|
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
|
|
if (previewTooltip) {
|
|
previewTooltip.hide();
|
|
}
|
|
});
|
|
|
|
const endDrag = (e) => {
|
|
if (!isDragging) return;
|
|
|
|
// Only stop propagation if we actually dragged
|
|
if (hasMoved) {
|
|
e.stopPropagation();
|
|
}
|
|
|
|
// Release pointer capture if still have the element
|
|
if (currentDragElement && activePointerId !== null) {
|
|
try {
|
|
currentDragElement.releasePointerCapture(activePointerId);
|
|
} catch (err) {
|
|
// Ignore errors if element is no longer in DOM
|
|
}
|
|
}
|
|
|
|
const wasDragging = hasMoved;
|
|
isDragging = false;
|
|
hasMoved = false;
|
|
activePointerId = null;
|
|
currentDragElement = null;
|
|
|
|
// Remove the class to restore normal cursor behavior
|
|
document.body.classList.remove('lm-lora-strength-dragging');
|
|
|
|
// Only call onDragEnd and re-render if we actually dragged
|
|
if (wasDragging) {
|
|
if (typeof onDragEnd === 'function') {
|
|
onDragEnd();
|
|
}
|
|
|
|
// Now do the re-render after drag is complete
|
|
if (renderFunction) {
|
|
renderFunction(widget.value, widget);
|
|
}
|
|
}
|
|
};
|
|
|
|
dragEl.addEventListener('pointerup', endDrag);
|
|
dragEl.addEventListener('pointercancel', endDrag);
|
|
}
|
|
|
|
// Function to initialize header drag for proportional strength adjustment
|
|
export function initHeaderDrag(headerEl, widget, renderFunction) {
|
|
let isDragging = false;
|
|
let activePointerId = null;
|
|
let initialX = 0;
|
|
let initialStrengths = [];
|
|
let currentHeaderElement = null;
|
|
let hasMoved = false;
|
|
|
|
// Add cursor style to indicate draggable
|
|
// Create a drag handler using pointer events for Vue DOM render mode compatibility
|
|
headerEl.addEventListener('pointerdown', (e) => {
|
|
// Skip if clicking on toggle or other interactive elements
|
|
if (e.target.closest('.lm-lora-toggle') ||
|
|
e.target.closest('input')) {
|
|
return;
|
|
}
|
|
|
|
// Only handle left mouse button (allow middle button for canvas drag, right button for context menu)
|
|
if (e.button !== 0) {
|
|
return;
|
|
}
|
|
|
|
// Store initial X position
|
|
initialX = e.clientX;
|
|
|
|
// Store initial strengths of all LoRAs
|
|
const lorasData = parseLoraValue(widget.value);
|
|
initialStrengths = lorasData.map(lora => ({
|
|
modelStrength: Number(lora.strength),
|
|
clipStrength: Number(lora.clipStrength)
|
|
}));
|
|
|
|
isDragging = true;
|
|
hasMoved = false;
|
|
activePointerId = e.pointerId;
|
|
currentHeaderElement = e.currentTarget;
|
|
|
|
// Capture pointer to receive all subsequent events regardless of stopPropagation
|
|
const target = e.currentTarget;
|
|
target.setPointerCapture(e.pointerId);
|
|
|
|
// Prevent text selection
|
|
e.preventDefault();
|
|
});
|
|
|
|
// Handle pointer move for dragging
|
|
headerEl.addEventListener('pointermove', (e) => {
|
|
if (!isDragging) return;
|
|
|
|
// Track if pointer moved significantly (more than 3 pixels)
|
|
if (Math.abs(e.clientX - initialX) > 3) {
|
|
hasMoved = true;
|
|
}
|
|
|
|
// Only stop propagation if we've started dragging (moved beyond threshold)
|
|
if (hasMoved) {
|
|
e.stopPropagation();
|
|
}
|
|
|
|
// Only process drag if we've moved beyond threshold
|
|
if (!hasMoved) return;
|
|
|
|
// Add class to body to enforce cursor style globally only after drag starts
|
|
document.body.classList.add('lm-lora-strength-dragging');
|
|
|
|
// Call the strength adjustment function without updating widget.value during drag
|
|
handleAllStrengthsDrag(initialStrengths, initialX, e, widget, false);
|
|
|
|
// Update strength inputs directly instead of re-rendering to avoid losing event listeners
|
|
const strengthInputs = currentHeaderElement.parentElement.querySelectorAll('.lm-lora-strength-input');
|
|
const lorasData = parseLoraValue(widget.value);
|
|
strengthInputs.forEach((input, index) => {
|
|
if (lorasData[index]) {
|
|
input.value = lorasData[index].strength.toFixed(2);
|
|
}
|
|
});
|
|
});
|
|
|
|
const endDrag = (e) => {
|
|
if (!isDragging) return;
|
|
|
|
// Only stop propagation if we actually dragged
|
|
if (hasMoved) {
|
|
e.stopPropagation();
|
|
}
|
|
|
|
// Release pointer capture if still have the element
|
|
if (currentHeaderElement && activePointerId !== null) {
|
|
try {
|
|
currentHeaderElement.releasePointerCapture(activePointerId);
|
|
} catch (err) {
|
|
// Ignore errors if element is no longer in DOM
|
|
}
|
|
}
|
|
|
|
const wasDragging = hasMoved;
|
|
isDragging = false;
|
|
hasMoved = false;
|
|
activePointerId = null;
|
|
currentHeaderElement = null;
|
|
|
|
// Remove the class to restore normal cursor behavior
|
|
document.body.classList.remove('lm-lora-strength-dragging');
|
|
|
|
// Only re-render if we actually dragged
|
|
if (wasDragging && renderFunction) {
|
|
renderFunction(widget.value, widget);
|
|
}
|
|
};
|
|
|
|
// Handle pointer up to end dragging
|
|
headerEl.addEventListener('pointerup', endDrag);
|
|
|
|
// Handle pointer cancel to end dragging
|
|
headerEl.addEventListener('pointercancel', endDrag);
|
|
}
|
|
|
|
// Function to initialize drag-and-drop for reordering
|
|
export function initReorderDrag(dragHandle, loraName, widget, renderFunction) {
|
|
let isDragging = false;
|
|
let activePointerId = null;
|
|
let draggedElement = null;
|
|
let dropIndicator = null;
|
|
let container = null;
|
|
let scale = 1;
|
|
let hasMoved = false;
|
|
|
|
dragHandle.addEventListener('pointerdown', (e) => {
|
|
e.preventDefault();
|
|
|
|
isDragging = true;
|
|
hasMoved = false;
|
|
activePointerId = e.pointerId;
|
|
draggedElement = dragHandle.closest('.lm-lora-entry');
|
|
container = draggedElement.parentElement;
|
|
|
|
// Capture pointer to receive all subsequent events regardless of stopPropagation
|
|
const target = e.currentTarget;
|
|
target.setPointerCapture(e.pointerId);
|
|
});
|
|
|
|
dragHandle.addEventListener('pointermove', (e) => {
|
|
if (!isDragging || !draggedElement) return;
|
|
|
|
// Track if pointer moved significantly (more than 3 pixels vertically)
|
|
if (Math.abs(e.movementY) > 3) {
|
|
hasMoved = true;
|
|
}
|
|
|
|
// Only stop propagation and process drag if we've moved beyond threshold
|
|
if (!hasMoved) return;
|
|
|
|
// Stop propagation and start drag visuals
|
|
e.stopPropagation();
|
|
|
|
// Add dragging class and visual feedback only after drag starts
|
|
if (!dropIndicator) {
|
|
draggedElement.classList.add('lm-lora-entry--dragging');
|
|
|
|
// 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.classList.add('lm-lora-reordering');
|
|
|
|
// Store workflow scale for accurate positioning
|
|
scale = app.canvas.ds.scale;
|
|
}
|
|
|
|
const targetIndex = getDropTargetIndex(container, e.clientY);
|
|
const entries = container.querySelectorAll('.lm-lora-entry, .lm-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';
|
|
}
|
|
}
|
|
});
|
|
|
|
dragHandle.addEventListener('pointerup', (e) => {
|
|
// Only stop propagation if we actually dragged
|
|
if (hasMoved) {
|
|
e.stopPropagation();
|
|
}
|
|
|
|
// Always reset cursor regardless of isDragging state
|
|
document.body.classList.remove('lm-lora-reordering');
|
|
|
|
if (!isDragging || !draggedElement) {
|
|
// Release pointer capture even if not dragging
|
|
const target = e.currentTarget;
|
|
if (activePointerId !== null) {
|
|
target.releasePointerCapture(activePointerId);
|
|
}
|
|
isDragging = false;
|
|
hasMoved = false;
|
|
activePointerId = null;
|
|
return;
|
|
}
|
|
|
|
// Release pointer capture
|
|
const target = e.currentTarget;
|
|
if (activePointerId !== null) {
|
|
target.releasePointerCapture(activePointerId);
|
|
}
|
|
|
|
const wasDragging = hasMoved;
|
|
isDragging = false;
|
|
hasMoved = false;
|
|
activePointerId = null;
|
|
|
|
// Only process reordering if we actually dragged
|
|
if (wasDragging) {
|
|
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('.lm-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
|
|
if (draggedElement) {
|
|
draggedElement.classList.remove('lm-lora-entry--dragging');
|
|
draggedElement = null;
|
|
}
|
|
|
|
if (dropIndicator && container) {
|
|
container.removeChild(dropIndicator);
|
|
// Restore original position
|
|
container.style.position = container._originalPosition || '';
|
|
delete container._originalPosition;
|
|
dropIndicator = null;
|
|
}
|
|
|
|
container = null;
|
|
});
|
|
|
|
dragHandle.addEventListener('pointercancel', (e) => {
|
|
// Only stop propagation if we actually dragged
|
|
if (hasMoved) {
|
|
e.stopPropagation();
|
|
}
|
|
|
|
// Always reset cursor regardless of isDragging state
|
|
document.body.classList.remove('lm-lora-reordering');
|
|
|
|
if (!isDragging || !draggedElement) {
|
|
// Release pointer capture even if not dragging
|
|
const target = e.currentTarget;
|
|
if (activePointerId !== null) {
|
|
target.releasePointerCapture(activePointerId);
|
|
}
|
|
isDragging = false;
|
|
hasMoved = false;
|
|
activePointerId = null;
|
|
return;
|
|
}
|
|
|
|
// Release pointer capture
|
|
const target = e.currentTarget;
|
|
if (activePointerId !== null) {
|
|
target.releasePointerCapture(activePointerId);
|
|
}
|
|
|
|
isDragging = false;
|
|
hasMoved = false;
|
|
activePointerId = null;
|
|
|
|
// Cleanup without reordering
|
|
if (draggedElement) {
|
|
draggedElement.classList.remove('lm-lora-entry--dragging');
|
|
draggedElement = null;
|
|
}
|
|
|
|
if (dropIndicator && container) {
|
|
container.removeChild(dropIndicator);
|
|
// Restore original position
|
|
container.style.position = container._originalPosition || '';
|
|
delete container._originalPosition;
|
|
dropIndicator = null;
|
|
}
|
|
|
|
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;
|
|
const isStrengthInputFocused =
|
|
event?.target?.classList?.contains('lm-lora-strength-input') ?? 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':
|
|
if (isStrengthInputFocused) {
|
|
break;
|
|
}
|
|
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
|
|
if (previewTooltip) {
|
|
previewTooltip.hide();
|
|
}
|
|
|
|
// Remove existing context menu if any
|
|
const existingMenu = document.querySelector('.lm-lora-context-menu');
|
|
if (existingMenu) {
|
|
existingMenu.remove();
|
|
}
|
|
|
|
const menu = document.createElement('div');
|
|
menu.className = 'lm-lora-context-menu';
|
|
menu.style.left = `${x}px`;
|
|
menu.style.top = `${y}px`;
|
|
|
|
// View on Civitai option with globe icon
|
|
const viewOnCivitaiOption = createMenuItem(
|
|
'View on Civitai',
|
|
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><line x1="2" y1="12" x2="22" y2="12"></line><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path></svg>',
|
|
async () => {
|
|
menu.remove();
|
|
document.removeEventListener('click', closeMenu);
|
|
|
|
try {
|
|
// Get Civitai URL from API
|
|
const response = await api.fetchApi(`/lm/loras/civitai-url?name=${encodeURIComponent(loraName)}`, {
|
|
method: 'GET'
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text();
|
|
throw new Error(errorText || 'Failed to get Civitai URL');
|
|
}
|
|
|
|
const data = await response.json();
|
|
if (data.success && data.civitai_url) {
|
|
// Open the URL in a new tab
|
|
window.open(data.civitai_url, '_blank');
|
|
} else {
|
|
// Show error message if no Civitai URL
|
|
showToast('This LoRA has no associated Civitai URL', 'warning');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error getting Civitai URL:', error);
|
|
showToast(error.message || 'Failed to get Civitai URL', 'error');
|
|
}
|
|
}
|
|
);
|
|
|
|
// Delete option with trash icon
|
|
const deleteOption = createMenuItem(
|
|
'Delete',
|
|
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18m-2 0v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6m3 0V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"></path></svg>',
|
|
() => {
|
|
menu.remove();
|
|
document.removeEventListener('click', closeMenu);
|
|
|
|
const lorasData = parseLoraValue(widget.value).filter(l => l.name !== loraName);
|
|
widget.value = formatLoraValue(lorasData);
|
|
|
|
if (widget.callback) {
|
|
widget.callback(widget.value);
|
|
}
|
|
|
|
// Re-render
|
|
if (renderFunction) {
|
|
renderFunction(widget.value, widget);
|
|
}
|
|
}
|
|
);
|
|
|
|
// New option: Copy Notes with note icon
|
|
const copyNotesOption = createMenuItem(
|
|
'Copy Notes',
|
|
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line><polyline points="10 9 9 9 8 9"></polyline></svg>',
|
|
async () => {
|
|
menu.remove();
|
|
document.removeEventListener('click', closeMenu);
|
|
|
|
try {
|
|
// Get notes from API
|
|
const response = await api.fetchApi(`/lm/loras/get-notes?name=${encodeURIComponent(loraName)}`, {
|
|
method: 'GET'
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text();
|
|
throw new Error(errorText || 'Failed to get notes');
|
|
}
|
|
|
|
const data = await response.json();
|
|
if (data.success) {
|
|
const notes = data.notes || '';
|
|
if (notes.trim()) {
|
|
await copyToClipboard(notes, 'Notes copied to clipboard');
|
|
} else {
|
|
showToast('No notes available for this LoRA', 'info');
|
|
}
|
|
} else {
|
|
throw new Error(data.error || 'Failed to get notes');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error getting notes:', error);
|
|
showToast(error.message || 'Failed to get notes', 'error');
|
|
}
|
|
}
|
|
);
|
|
|
|
// New option: Copy Trigger Words with tag icon
|
|
const copyTriggerWordsOption = createMenuItem(
|
|
'Copy Trigger Words',
|
|
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></svg>',
|
|
async () => {
|
|
menu.remove();
|
|
document.removeEventListener('click', closeMenu);
|
|
|
|
try {
|
|
// Get trigger words from API
|
|
const response = await api.fetchApi(`/lm/loras/get-trigger-words?name=${encodeURIComponent(loraName)}`, {
|
|
method: 'GET'
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text();
|
|
throw new Error(errorText || 'Failed to get trigger words');
|
|
}
|
|
|
|
const data = await response.json();
|
|
if (data.success) {
|
|
const triggerWords = data.trigger_words || [];
|
|
if (triggerWords.length > 0) {
|
|
// Join trigger words with commas
|
|
const triggerWordsText = triggerWords.join(', ');
|
|
await copyToClipboard(triggerWordsText, 'Trigger words copied to clipboard');
|
|
} else {
|
|
showToast('No trigger words available for this LoRA', 'info');
|
|
}
|
|
} else {
|
|
throw new Error(data.error || 'Failed to get trigger words');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error getting trigger words:', error);
|
|
showToast(error.message || 'Failed to get trigger words', 'error');
|
|
}
|
|
}
|
|
);
|
|
|
|
// Save recipe option with bookmark icon
|
|
const saveOption = createMenuItem(
|
|
'Save Recipe',
|
|
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"></path></svg>',
|
|
() => {
|
|
menu.remove();
|
|
document.removeEventListener('click', closeMenu);
|
|
saveRecipeDirectly();
|
|
}
|
|
);
|
|
|
|
// Move Up option with arrow up icon
|
|
const moveUpOption = createMenuItem(
|
|
'Move Up',
|
|
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 15l-6-6-6 6"></path></svg>',
|
|
() => {
|
|
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',
|
|
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 9l6 6 6-6"></path></svg>',
|
|
() => {
|
|
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',
|
|
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 11l-5-5-5 5M17 18l-5-5-5 5"></path></svg>',
|
|
() => {
|
|
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',
|
|
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M7 13l5 5 5-5M7 6l5 5 5-5"></path></svg>',
|
|
() => {
|
|
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('hr');
|
|
|
|
// Add second separator
|
|
const separator2 = document.createElement('hr');
|
|
|
|
// Add separator for order options
|
|
const orderSeparator = document.createElement('hr');
|
|
|
|
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);
|
|
menu.appendChild(saveOption);
|
|
|
|
document.body.appendChild(menu);
|
|
|
|
// Close menu when clicking outside
|
|
const closeMenu = (e) => {
|
|
if (!menu.contains(e.target)) {
|
|
menu.remove();
|
|
document.removeEventListener('click', closeMenu);
|
|
}
|
|
};
|
|
setTimeout(() => document.addEventListener('click', closeMenu), 0);
|
|
}
|