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;
}
// 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;
}
// 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',
'',
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',
'',
() => {
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',
'',
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',
'',
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',
'',
() => {
menu.remove();
document.removeEventListener('click', closeMenu);
saveRecipeDirectly();
}
);
// 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('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);
}