mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-05-06 16:36:45 -03:00
fix(trigger-words): support stable inline editing
This commit is contained in:
@@ -297,6 +297,8 @@ export function setupTriggerWordsEditMode() {
|
||||
// Disable click-to-copy and show delete buttons
|
||||
triggerWordTags.forEach(tag => {
|
||||
tag.onclick = null;
|
||||
tag.addEventListener('click', startEditTriggerWord);
|
||||
tag.title = translate('modals.model.triggerWords.editWord');
|
||||
const copyIcon = tag.querySelector('.trigger-word-copy');
|
||||
const deleteBtn = tag.querySelector('.metadata-delete-btn');
|
||||
|
||||
@@ -353,6 +355,7 @@ export function setupTriggerWordsEditMode() {
|
||||
// If canceling, restore original trigger words
|
||||
restoreOriginalTriggerWords(triggerWordsSection, originalTriggerWords);
|
||||
} else {
|
||||
commitActiveTriggerWordEdit(triggerWordsSection);
|
||||
// If saving, reset UI state on current trigger words
|
||||
resetTriggerWordsUIState(triggerWordsSection);
|
||||
// Reset the skip restore flag
|
||||
@@ -432,15 +435,18 @@ function deleteTriggerWord(e) {
|
||||
* @param {HTMLElement} section - The trigger words section
|
||||
*/
|
||||
function resetTriggerWordsUIState(section) {
|
||||
commitActiveTriggerWordEdit(section);
|
||||
|
||||
const triggerWordTags = section.querySelectorAll('.trigger-word-tag');
|
||||
|
||||
triggerWordTags.forEach(tag => {
|
||||
const word = tag.dataset.word;
|
||||
const copyIcon = tag.querySelector('.trigger-word-copy');
|
||||
const deleteBtn = tag.querySelector('.metadata-delete-btn');
|
||||
|
||||
// Restore click-to-copy functionality
|
||||
tag.removeEventListener('click', startEditTriggerWord);
|
||||
tag.onclick = () => copyTriggerWord(tag.dataset.word);
|
||||
tag.title = translate('modals.model.triggerWords.copyWord');
|
||||
|
||||
// Show copy icon, hide delete button
|
||||
if (copyIcon) copyIcon.style.display = '';
|
||||
@@ -474,25 +480,158 @@ function restoreOriginalTriggerWords(section, originalWords) {
|
||||
|
||||
// Recreate original tags
|
||||
originalWords.forEach(word => {
|
||||
const tag = document.createElement('div');
|
||||
tag.className = 'trigger-word-tag';
|
||||
tag.dataset.word = word;
|
||||
tag.onclick = () => copyTriggerWord(tag.dataset.word);
|
||||
|
||||
const escapedWord = escapeHtml(word);
|
||||
tag.innerHTML = `
|
||||
<span class="trigger-word-content">${escapedWord}</span>
|
||||
<span class="trigger-word-copy">
|
||||
<i class="fas fa-copy"></i>
|
||||
</span>
|
||||
<button class="metadata-delete-btn" style="display:none;" onclick="event.stopPropagation();">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
`;
|
||||
tagsContainer.appendChild(tag);
|
||||
tagsContainer.appendChild(createTriggerWordTag(word, false));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a trigger word tag element
|
||||
* @param {string} word - Trigger word
|
||||
* @param {boolean} isEditMode - Whether the tag should be editable
|
||||
* @returns {HTMLElement} Tag element
|
||||
*/
|
||||
function createTriggerWordTag(word, isEditMode = false) {
|
||||
const tag = document.createElement('div');
|
||||
tag.className = 'trigger-word-tag';
|
||||
tag.dataset.word = word;
|
||||
tag.title = translate(isEditMode ? 'modals.model.triggerWords.editWord' : 'modals.model.triggerWords.copyWord');
|
||||
|
||||
const escapedWord = escapeHtml(word);
|
||||
tag.innerHTML = `
|
||||
<span class="trigger-word-content">${escapedWord}</span>
|
||||
<span class="trigger-word-copy" style="${isEditMode ? 'display:none;' : ''}">
|
||||
<i class="fas fa-copy"></i>
|
||||
</span>
|
||||
<button class="metadata-delete-btn" style="${isEditMode ? '' : 'display:none;'}" onclick="event.stopPropagation();" title="${translate('modals.model.triggerWords.deleteWord')}">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
`;
|
||||
|
||||
const deleteBtn = tag.querySelector('.metadata-delete-btn');
|
||||
deleteBtn.addEventListener('click', deleteTriggerWord);
|
||||
|
||||
if (isEditMode) {
|
||||
tag.addEventListener('click', startEditTriggerWord);
|
||||
} else {
|
||||
tag.onclick = () => copyTriggerWord(tag.dataset.word);
|
||||
}
|
||||
|
||||
return tag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a trigger word against existing tags
|
||||
* @param {string} word - Trigger word
|
||||
* @param {HTMLElement} tagsContainer - Tags container
|
||||
* @param {HTMLElement|null} currentTag - Tag being edited, if any
|
||||
* @returns {boolean} Whether the word is valid
|
||||
*/
|
||||
function validateTriggerWord(word, tagsContainer, currentTag = null) {
|
||||
if (word.split(/\s+/).length > MAX_WORDS_PER_TRIGGER_GROUP) {
|
||||
showToast('toast.triggerWords.tooLong', {}, 'error');
|
||||
return false;
|
||||
}
|
||||
|
||||
const currentTags = tagsContainer.querySelectorAll('.trigger-word-tag');
|
||||
const existingWords = Array.from(currentTags)
|
||||
.filter(tag => tag !== currentTag)
|
||||
.map(tag => tag.dataset.word);
|
||||
|
||||
if (existingWords.includes(word)) {
|
||||
showToast('toast.triggerWords.alreadyExists', {}, 'error');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start inline editing for a trigger word tag
|
||||
* @param {Event} e - Click event
|
||||
*/
|
||||
function startEditTriggerWord(e) {
|
||||
if (e.target.closest('.metadata-delete-btn') || e.target.closest('.trigger-word-edit-input')) return;
|
||||
|
||||
const tag = this.closest('.trigger-word-tag');
|
||||
const section = tag?.closest('.trigger-words');
|
||||
if (!tag || !section?.classList.contains('edit-mode') || tag.classList.contains('is-editing')) return;
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
commitActiveTriggerWordEdit(section);
|
||||
|
||||
const content = tag.querySelector('.trigger-word-content');
|
||||
const originalWord = tag.dataset.word;
|
||||
const originalRect = tag.getBoundingClientRect();
|
||||
if (originalRect.width > 0) {
|
||||
tag.style.setProperty('--trigger-word-edit-width', `${Math.ceil(originalRect.width)}px`);
|
||||
}
|
||||
if (originalRect.height > 0) {
|
||||
tag.style.setProperty('--trigger-word-edit-height', `${Math.ceil(originalRect.height)}px`);
|
||||
}
|
||||
|
||||
const editor = document.createElement('textarea');
|
||||
editor.className = 'trigger-word-edit-input';
|
||||
editor.rows = 1;
|
||||
editor.value = originalWord;
|
||||
editor.setAttribute('aria-label', translate('modals.model.triggerWords.editWord'));
|
||||
editor.placeholder = translate('modals.model.triggerWords.editPlaceholder');
|
||||
|
||||
let finished = false;
|
||||
const finish = (shouldCommit) => {
|
||||
if (finished) return;
|
||||
finished = true;
|
||||
|
||||
const nextWord = editor.value.trim().replace(/\s*\n+\s*/g, ' ');
|
||||
if (shouldCommit && nextWord && nextWord !== originalWord) {
|
||||
const tagsContainer = tag.closest('.trigger-words-tags');
|
||||
if (tagsContainer && validateTriggerWord(nextWord, tagsContainer, tag)) {
|
||||
tag.dataset.word = nextWord;
|
||||
content.textContent = nextWord;
|
||||
}
|
||||
}
|
||||
|
||||
editor.remove();
|
||||
content.style.display = '';
|
||||
tag.classList.remove('is-editing');
|
||||
tag.style.removeProperty('--trigger-word-edit-width');
|
||||
tag.style.removeProperty('--trigger-word-edit-height');
|
||||
updateTrainedWordsDropdown();
|
||||
};
|
||||
|
||||
editor.addEventListener('click', event => event.stopPropagation());
|
||||
editor.addEventListener('keydown', event => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
finish(true);
|
||||
} else if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
finish(false);
|
||||
}
|
||||
});
|
||||
editor.addEventListener('blur', () => finish(true));
|
||||
|
||||
editor.style.visibility = 'hidden';
|
||||
content.after(editor);
|
||||
tag.classList.add('is-editing');
|
||||
content.style.display = 'none';
|
||||
editor.style.visibility = '';
|
||||
editor.focus();
|
||||
editor.select();
|
||||
}
|
||||
|
||||
/**
|
||||
* Commit an active inline trigger word edit if one exists
|
||||
* @param {HTMLElement} section - Trigger words section
|
||||
*/
|
||||
function commitActiveTriggerWordEdit(section) {
|
||||
const input = section.querySelector('.trigger-word-edit-input');
|
||||
if (input) {
|
||||
input.dispatchEvent(new FocusEvent('blur'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new trigger word
|
||||
* @param {string} word - Trigger word to add
|
||||
@@ -525,12 +664,6 @@ function addNewTriggerWord(word) {
|
||||
noTriggerWordsMsg.style.display = 'none';
|
||||
}
|
||||
|
||||
// Validation: Check length
|
||||
if (word.split(/\s+/).length > MAX_WORDS_PER_TRIGGER_GROUP) {
|
||||
showToast('toast.triggerWords.tooLong', {}, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validation: Check total number
|
||||
const currentTags = tagsContainer.querySelectorAll('.trigger-word-tag');
|
||||
if (currentTags.length >= MAX_TRIGGER_WORD_GROUPS) {
|
||||
@@ -538,33 +671,9 @@ function addNewTriggerWord(word) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Validation: Check for duplicates
|
||||
const existingWords = Array.from(currentTags).map(tag => tag.dataset.word);
|
||||
if (existingWords.includes(word)) {
|
||||
showToast('toast.triggerWords.alreadyExists', {}, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create new tag
|
||||
const newTag = document.createElement('div');
|
||||
newTag.className = 'trigger-word-tag';
|
||||
newTag.dataset.word = word;
|
||||
|
||||
const escapedWord = escapeHtml(word);
|
||||
newTag.innerHTML = `
|
||||
<span class="trigger-word-content">${escapedWord}</span>
|
||||
<span class="trigger-word-copy" style="display:none;">
|
||||
<i class="fas fa-copy"></i>
|
||||
</span>
|
||||
<button class="metadata-delete-btn" onclick="event.stopPropagation();">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
`;
|
||||
|
||||
// Add event listener to delete button
|
||||
const deleteBtn = newTag.querySelector('.metadata-delete-btn');
|
||||
deleteBtn.addEventListener('click', deleteTriggerWord);
|
||||
if (!validateTriggerWord(word, tagsContainer)) return;
|
||||
|
||||
const newTag = createTriggerWordTag(word, triggerWordsSection.classList.contains('edit-mode'));
|
||||
tagsContainer.appendChild(newTag);
|
||||
|
||||
// Update status of items in the trained words dropdown
|
||||
@@ -636,6 +745,8 @@ async function saveTriggerWords() {
|
||||
const filePath = editBtn.dataset.filePath;
|
||||
const triggerWordsSection = editBtn.closest('.trigger-words');
|
||||
|
||||
commitActiveTriggerWordEdit(triggerWordsSection);
|
||||
|
||||
// Auto-commit any pending input to prevent data loss
|
||||
const input = triggerWordsSection.querySelector('.metadata-input');
|
||||
if (input && input.value.trim()) {
|
||||
|
||||
Reference in New Issue
Block a user