mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-05-06 16:36:45 -03:00
fix(trigger-words): edit tag on double click
This commit is contained in:
@@ -10,6 +10,7 @@ import { escapeAttribute, escapeHtml } from './utils.js';
|
|||||||
|
|
||||||
const MAX_WORDS_PER_TRIGGER_GROUP = 500;
|
const MAX_WORDS_PER_TRIGGER_GROUP = 500;
|
||||||
const MAX_TRIGGER_WORD_GROUPS = 100;
|
const MAX_TRIGGER_WORD_GROUPS = 100;
|
||||||
|
const TRIGGER_WORD_CLICK_DELAY_MS = 220;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch trained words for a model
|
* Fetch trained words for a model
|
||||||
@@ -226,7 +227,7 @@ export function renderTriggerWords(words, filePath) {
|
|||||||
const escapedWord = escapeHtml(word);
|
const escapedWord = escapeHtml(word);
|
||||||
const escapedAttr = escapeAttribute(word);
|
const escapedAttr = escapeAttribute(word);
|
||||||
return `
|
return `
|
||||||
<div class="trigger-word-tag" data-word="${escapedAttr}" onclick="copyTriggerWord(this.dataset.word)" title="${translate('modals.model.triggerWords.copyWord')}">
|
<div class="trigger-word-tag" data-word="${escapedAttr}" title="${translate('modals.model.triggerWords.copyWord')}">
|
||||||
<span class="trigger-word-content">${escapedWord}</span>
|
<span class="trigger-word-content">${escapedWord}</span>
|
||||||
<span class="trigger-word-copy">
|
<span class="trigger-word-copy">
|
||||||
<i class="fas fa-copy"></i>
|
<i class="fas fa-copy"></i>
|
||||||
@@ -264,6 +265,8 @@ export function setupTriggerWordsEditMode() {
|
|||||||
const editBtn = document.querySelector('.edit-trigger-words-btn');
|
const editBtn = document.querySelector('.edit-trigger-words-btn');
|
||||||
if (!editBtn) return;
|
if (!editBtn) return;
|
||||||
|
|
||||||
|
document.querySelectorAll('.trigger-word-tag').forEach(setupDisplayTriggerWordTag);
|
||||||
|
|
||||||
editBtn.addEventListener('click', async function () {
|
editBtn.addEventListener('click', async function () {
|
||||||
const triggerWordsSection = this.closest('.trigger-words');
|
const triggerWordsSection = this.closest('.trigger-words');
|
||||||
const isEditMode = triggerWordsSection.classList.toggle('edit-mode');
|
const isEditMode = triggerWordsSection.classList.toggle('edit-mode');
|
||||||
@@ -296,7 +299,7 @@ export function setupTriggerWordsEditMode() {
|
|||||||
|
|
||||||
// Disable click-to-copy and show delete buttons
|
// Disable click-to-copy and show delete buttons
|
||||||
triggerWordTags.forEach(tag => {
|
triggerWordTags.forEach(tag => {
|
||||||
tag.onclick = null;
|
teardownDisplayTriggerWordTag(tag);
|
||||||
tag.addEventListener('click', startEditTriggerWord);
|
tag.addEventListener('click', startEditTriggerWord);
|
||||||
tag.title = translate('modals.model.triggerWords.editWord');
|
tag.title = translate('modals.model.triggerWords.editWord');
|
||||||
const copyIcon = tag.querySelector('.trigger-word-copy');
|
const copyIcon = tag.querySelector('.trigger-word-copy');
|
||||||
@@ -342,6 +345,12 @@ export function setupTriggerWordsEditMode() {
|
|||||||
// Focus the input
|
// Focus the input
|
||||||
addForm.querySelector('input').focus();
|
addForm.querySelector('input').focus();
|
||||||
|
|
||||||
|
const pendingEditTag = triggerWordsSection._pendingTriggerWordEditTag;
|
||||||
|
delete triggerWordsSection._pendingTriggerWordEditTag;
|
||||||
|
if (pendingEditTag && document.contains(pendingEditTag)) {
|
||||||
|
startEditTriggerWord.call(pendingEditTag, { target: pendingEditTag, preventDefault() { }, stopPropagation() { } });
|
||||||
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
this.innerHTML = '<i class="fas fa-pencil-alt"></i>'; // Change back to edit icon
|
this.innerHTML = '<i class="fas fa-pencil-alt"></i>'; // Change back to edit icon
|
||||||
this.title = translate('modals.model.triggerWords.edit');
|
this.title = translate('modals.model.triggerWords.edit');
|
||||||
@@ -445,7 +454,7 @@ function resetTriggerWordsUIState(section) {
|
|||||||
|
|
||||||
// Restore click-to-copy functionality
|
// Restore click-to-copy functionality
|
||||||
tag.removeEventListener('click', startEditTriggerWord);
|
tag.removeEventListener('click', startEditTriggerWord);
|
||||||
tag.onclick = () => copyTriggerWord(tag.dataset.word);
|
setupDisplayTriggerWordTag(tag);
|
||||||
tag.title = translate('modals.model.triggerWords.copyWord');
|
tag.title = translate('modals.model.triggerWords.copyWord');
|
||||||
|
|
||||||
// Show copy icon, hide delete button
|
// Show copy icon, hide delete button
|
||||||
@@ -513,12 +522,90 @@ function createTriggerWordTag(word, isEditMode = false) {
|
|||||||
if (isEditMode) {
|
if (isEditMode) {
|
||||||
tag.addEventListener('click', startEditTriggerWord);
|
tag.addEventListener('click', startEditTriggerWord);
|
||||||
} else {
|
} else {
|
||||||
tag.onclick = () => copyTriggerWord(tag.dataset.word);
|
setupDisplayTriggerWordTag(tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
return tag;
|
return tag;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up display-mode click-to-copy and double-click-to-edit behavior
|
||||||
|
* @param {HTMLElement} tag - Trigger word tag
|
||||||
|
*/
|
||||||
|
function setupDisplayTriggerWordTag(tag) {
|
||||||
|
teardownDisplayTriggerWordTag(tag);
|
||||||
|
|
||||||
|
tag.addEventListener('click', handleDisplayTriggerWordClick);
|
||||||
|
tag.addEventListener('dblclick', handleDisplayTriggerWordDoubleClick);
|
||||||
|
tag.title = translate('modals.model.triggerWords.copyWord');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove display-mode handlers and pending copy action
|
||||||
|
* @param {HTMLElement} tag - Trigger word tag
|
||||||
|
*/
|
||||||
|
function teardownDisplayTriggerWordTag(tag) {
|
||||||
|
if (tag.dataset.copyTimerId) {
|
||||||
|
clearTimeout(Number(tag.dataset.copyTimerId));
|
||||||
|
delete tag.dataset.copyTimerId;
|
||||||
|
}
|
||||||
|
tag.onclick = null;
|
||||||
|
tag.removeEventListener('click', handleDisplayTriggerWordClick);
|
||||||
|
tag.removeEventListener('dblclick', handleDisplayTriggerWordDoubleClick);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy trigger word after a short delay so dblclick can cancel it
|
||||||
|
* @param {MouseEvent} e - Click event
|
||||||
|
*/
|
||||||
|
function handleDisplayTriggerWordClick(e) {
|
||||||
|
if (e.target.closest('.metadata-delete-btn') || e.target.closest('.trigger-word-edit-input')) return;
|
||||||
|
|
||||||
|
const tag = this.closest('.trigger-word-tag');
|
||||||
|
if (!tag || tag.closest('.trigger-words')?.classList.contains('edit-mode')) return;
|
||||||
|
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
if (tag.dataset.copyTimerId) {
|
||||||
|
clearTimeout(Number(tag.dataset.copyTimerId));
|
||||||
|
}
|
||||||
|
|
||||||
|
const timerId = window.setTimeout(() => {
|
||||||
|
delete tag.dataset.copyTimerId;
|
||||||
|
copyTriggerWord(tag.dataset.word);
|
||||||
|
}, TRIGGER_WORD_CLICK_DELAY_MS);
|
||||||
|
tag.dataset.copyTimerId = String(timerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enter edit mode and start editing the double-clicked trigger word
|
||||||
|
* @param {MouseEvent} e - Double-click event
|
||||||
|
*/
|
||||||
|
function handleDisplayTriggerWordDoubleClick(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');
|
||||||
|
const editBtn = section?.querySelector('.edit-trigger-words-btn');
|
||||||
|
if (!tag || !section || !editBtn) return;
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
if (tag.dataset.copyTimerId) {
|
||||||
|
clearTimeout(Number(tag.dataset.copyTimerId));
|
||||||
|
delete tag.dataset.copyTimerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!section.classList.contains('edit-mode')) {
|
||||||
|
section._pendingTriggerWordEditTag = tag;
|
||||||
|
editBtn.click();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
startEditTriggerWord.call(tag, e);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate a trigger word against existing tags
|
* Validate a trigger word against existing tags
|
||||||
* @param {string} word - Trigger word
|
* @param {string} word - Trigger word
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ describe("TriggerWords HTML Escaping", () => {
|
|||||||
expect(html).toContain('data-word="word'with'quotes"');
|
expect(html).toContain('data-word="word'with'quotes"');
|
||||||
expect(html).toContain('data-word="<tag>"');
|
expect(html).toContain('data-word="<tag>"');
|
||||||
|
|
||||||
// Check for the onclick handler
|
// Copy/edit handlers are attached by setupTriggerWordsEditMode, not inline HTML.
|
||||||
expect(html).toContain('onclick="copyTriggerWord(this.dataset.word)"');
|
expect(html).not.toContain('onclick="copyTriggerWord');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ describe("TriggerWords inline editing", () => {
|
|||||||
let renderTriggerWords;
|
let renderTriggerWords;
|
||||||
let setupTriggerWordsEditMode;
|
let setupTriggerWordsEditMode;
|
||||||
let showToast;
|
let showToast;
|
||||||
|
let copyToClipboard;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
document.body.innerHTML = '';
|
document.body.innerHTML = '';
|
||||||
@@ -46,6 +47,7 @@ describe("TriggerWords inline editing", () => {
|
|||||||
renderTriggerWords = module.renderTriggerWords;
|
renderTriggerWords = module.renderTriggerWords;
|
||||||
setupTriggerWordsEditMode = module.setupTriggerWordsEditMode;
|
setupTriggerWordsEditMode = module.setupTriggerWordsEditMode;
|
||||||
showToast = uiHelpers.showToast;
|
showToast = uiHelpers.showToast;
|
||||||
|
copyToClipboard = uiHelpers.copyToClipboard;
|
||||||
});
|
});
|
||||||
|
|
||||||
async function enterEditMode(words = ["alpha", "beta"]) {
|
async function enterEditMode(words = ["alpha", "beta"]) {
|
||||||
@@ -81,6 +83,24 @@ describe("TriggerWords inline editing", () => {
|
|||||||
expect(document.querySelector('.trigger-word-edit-input')).toBeNull();
|
expect(document.querySelector('.trigger-word-edit-input')).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("enters edit mode and edits the double-clicked tag from display mode without copying", async () => {
|
||||||
|
document.body.innerHTML = renderTriggerWords(["alpha", "beta"], "test.safetensors");
|
||||||
|
setupTriggerWordsEditMode();
|
||||||
|
|
||||||
|
const firstTag = document.querySelector('.trigger-word-tag');
|
||||||
|
firstTag.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
|
||||||
|
firstTag.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
|
||||||
|
firstTag.dispatchEvent(new MouseEvent('dblclick', { bubbles: true, cancelable: true }));
|
||||||
|
|
||||||
|
await vi.waitFor(() => {
|
||||||
|
expect(document.querySelector('.trigger-words').classList.contains('edit-mode')).toBe(true);
|
||||||
|
expect(firstTag.querySelector('.trigger-word-edit-input')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 260));
|
||||||
|
expect(copyToClipboard).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it("keeps the original word and shows a toast when editing to a duplicate", async () => {
|
it("keeps the original word and shows a toast when editing to a duplicate", async () => {
|
||||||
await enterEditMode();
|
await enterEditMode();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user