mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-08-18 11:31:26 -03:00
fix(ui): ignore internal card drags in model card preview drop (#1034)
Tag move-to-folder drags with a custom dataTransfer MIME type so card preview-drop handlers skip them entirely (no highlight, no upload), and mark the preview image non-draggable so the browser no longer synthesizes a File payload when a drag starts on the image. Fixes card-on-card drops and click-jitter self-drops replacing the preview with itself.
This commit is contained in:
@@ -9,6 +9,7 @@ import { bulkManager } from '../managers/BulkManager.js';
|
|||||||
import { showToast } from '../utils/uiHelpers.js';
|
import { showToast } from '../utils/uiHelpers.js';
|
||||||
import { performFolderUpdateCheck } from '../utils/updateCheckHelpers.js';
|
import { performFolderUpdateCheck } from '../utils/updateCheckHelpers.js';
|
||||||
import { escapeHtml, escapeAttribute } from './shared/utils.js';
|
import { escapeHtml, escapeAttribute } from './shared/utils.js';
|
||||||
|
import { MODEL_CARD_DRAG_MIME_TYPE } from '../utils/constants.js';
|
||||||
|
|
||||||
export class SidebarManager {
|
export class SidebarManager {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -252,6 +253,9 @@ export class SidebarManager {
|
|||||||
if (dataTransfer) {
|
if (dataTransfer) {
|
||||||
dataTransfer.effectAllowed = 'move';
|
dataTransfer.effectAllowed = 'move';
|
||||||
dataTransfer.setData('text/plain', filePaths.join(','));
|
dataTransfer.setData('text/plain', filePaths.join(','));
|
||||||
|
// Tag the drag as an internal card drag so preview-drop handlers on
|
||||||
|
// other cards ignore it (no highlight, no preview replacement).
|
||||||
|
dataTransfer.setData(MODEL_CARD_DRAG_MIME_TYPE, filePaths.join(','));
|
||||||
try {
|
try {
|
||||||
dataTransfer.setData('application/json', JSON.stringify({ filePaths }));
|
dataTransfer.setData('application/json', JSON.stringify({ filePaths }));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { state, getCurrentPageState } from '../../state/index.js';
|
|||||||
import { showModelModal } from './ModelModal.js';
|
import { showModelModal } from './ModelModal.js';
|
||||||
import { bulkManager } from '../../managers/BulkManager.js';
|
import { bulkManager } from '../../managers/BulkManager.js';
|
||||||
import { modalManager } from '../../managers/ModalManager.js';
|
import { modalManager } from '../../managers/ModalManager.js';
|
||||||
import { NSFW_LEVELS, getBaseModelAbbreviation, getSubTypeAbbreviation, getMatureBlurThreshold, MODEL_SUBTYPE_DISPLAY_NAMES } from '../../utils/constants.js';
|
import { NSFW_LEVELS, getBaseModelAbbreviation, getSubTypeAbbreviation, getMatureBlurThreshold, MODEL_SUBTYPE_DISPLAY_NAMES, MODEL_CARD_DRAG_MIME_TYPE } from '../../utils/constants.js';
|
||||||
import { MODEL_TYPES } from '../../api/apiConfig.js';
|
import { MODEL_TYPES } from '../../api/apiConfig.js';
|
||||||
import { getModelApiClient } from '../../api/modelApiFactory.js';
|
import { getModelApiClient } from '../../api/modelApiFactory.js';
|
||||||
import { showDeleteModal } from '../../utils/modalUtils.js';
|
import { showDeleteModal } from '../../utils/modalUtils.js';
|
||||||
@@ -455,6 +455,9 @@ function showExampleAccessModal(card, modelType) {
|
|||||||
export function createModelCard(model, modelType) {
|
export function createModelCard(model, modelType) {
|
||||||
const card = document.createElement('div');
|
const card = document.createElement('div');
|
||||||
card.className = 'model-card'; // Reuse the same class for styling
|
card.className = 'model-card'; // Reuse the same class for styling
|
||||||
|
// Always draggable (move-to-folder in the sidebar). Accidental micro-drags
|
||||||
|
// from click jitter are rendered harmless by the preview-drop handlers
|
||||||
|
// below, which ignore internal card drags via MODEL_CARD_DRAG_MIME_TYPE.
|
||||||
card.draggable = true;
|
card.draggable = true;
|
||||||
card.dataset.sha256 = model.sha256;
|
card.dataset.sha256 = model.sha256;
|
||||||
card.dataset.filepath = model.file_path;
|
card.dataset.filepath = model.file_path;
|
||||||
@@ -647,7 +650,7 @@ export function createModelCard(model, modelType) {
|
|||||||
<div class="card-preview ${shouldBlur ? 'blurred' : ''}">
|
<div class="card-preview ${shouldBlur ? 'blurred' : ''}">
|
||||||
${isVideo ?
|
${isVideo ?
|
||||||
`<video ${videoAttrs.join(' ')} style="pointer-events: none;"></video>` :
|
`<video ${videoAttrs.join(' ')} style="pointer-events: none;"></video>` :
|
||||||
`<img src="${versionedPreviewUrl}" alt="${model.model_name}" onerror="this.onerror=null; this.src='/loras_static/images/no-preview.png'">`
|
`<img draggable="false" src="${versionedPreviewUrl}" alt="${model.model_name}" onerror="this.onerror=null; this.src='/loras_static/images/no-preview.png'">`
|
||||||
}
|
}
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
${shouldBlur ?
|
${shouldBlur ?
|
||||||
@@ -741,6 +744,11 @@ export function createModelCard(model, modelType) {
|
|||||||
|
|
||||||
// Dropping an image/video onto the card replaces the model preview via the
|
// Dropping an image/video onto the card replaces the model preview via the
|
||||||
// existing replace-preview endpoint (overwrites file on disk, refreshes card).
|
// existing replace-preview endpoint (overwrites file on disk, refreshes card).
|
||||||
|
// Internal card drags (move-to-folder) are tagged with a custom MIME type by
|
||||||
|
// SidebarManager and must be ignored here entirely: no highlight, no upload.
|
||||||
|
const isInternalCardDrag = (event) =>
|
||||||
|
Boolean(event.dataTransfer?.types?.includes(MODEL_CARD_DRAG_MIME_TYPE));
|
||||||
|
|
||||||
const preventDragDefaults = (event) => {
|
const preventDragDefaults = (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
@@ -748,17 +756,20 @@ export function createModelCard(model, modelType) {
|
|||||||
|
|
||||||
['dragenter', 'dragover'].forEach((eventName) => {
|
['dragenter', 'dragover'].forEach((eventName) => {
|
||||||
card.addEventListener(eventName, (event) => {
|
card.addEventListener(eventName, (event) => {
|
||||||
|
if (isInternalCardDrag(event)) return;
|
||||||
preventDragDefaults(event);
|
preventDragDefaults(event);
|
||||||
card.classList.add('drag-over');
|
card.classList.add('drag-over');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
card.addEventListener('dragleave', (event) => {
|
card.addEventListener('dragleave', (event) => {
|
||||||
|
if (isInternalCardDrag(event)) return;
|
||||||
preventDragDefaults(event);
|
preventDragDefaults(event);
|
||||||
card.classList.remove('drag-over');
|
card.classList.remove('drag-over');
|
||||||
});
|
});
|
||||||
|
|
||||||
card.addEventListener('drop', (event) => {
|
card.addEventListener('drop', (event) => {
|
||||||
|
if (isInternalCardDrag(event)) return;
|
||||||
preventDragDefaults(event);
|
preventDragDefaults(event);
|
||||||
card.classList.remove('drag-over');
|
card.classList.remove('drag-over');
|
||||||
|
|
||||||
|
|||||||
@@ -87,6 +87,10 @@ export const BASE_MODELS = {
|
|||||||
UNKNOWN: "Other"
|
UNKNOWN: "Other"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Custom dataTransfer MIME type tagging internal model-card drags (move-to-folder).
|
||||||
|
// Preview-drop handlers use it to ignore drags that did not come from the OS file system.
|
||||||
|
export const MODEL_CARD_DRAG_MIME_TYPE = 'application/x-lora-manager-model-card';
|
||||||
|
|
||||||
// Model sub-type display names (new canonical field: sub_type)
|
// Model sub-type display names (new canonical field: sub_type)
|
||||||
export const MODEL_SUBTYPE_DISPLAY_NAMES = {
|
export const MODEL_SUBTYPE_DISPLAY_NAMES = {
|
||||||
// LoRA sub-types
|
// LoRA sub-types
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
import { MODEL_CARD_DRAG_MIME_TYPE } from '../../../static/js/utils/constants.js';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
MODEL_CARD_MODULE,
|
MODEL_CARD_MODULE,
|
||||||
@@ -108,9 +109,9 @@ describe('ModelCard drag & drop preview upload', () => {
|
|||||||
return createModelCard(model, 'loras');
|
return createModelCard(model, 'loras');
|
||||||
}
|
}
|
||||||
|
|
||||||
function dispatchDrop(card, files) {
|
function dispatchDrop(card, files, types = []) {
|
||||||
const event = new Event('drop', { bubbles: true, cancelable: true });
|
const event = new Event('drop', { bubbles: true, cancelable: true });
|
||||||
Object.defineProperty(event, 'dataTransfer', { value: { files } });
|
Object.defineProperty(event, 'dataTransfer', { value: { files, types } });
|
||||||
card.dispatchEvent(event);
|
card.dispatchEvent(event);
|
||||||
return event;
|
return event;
|
||||||
}
|
}
|
||||||
@@ -179,4 +180,41 @@ describe('ModelCard drag & drop preview upload', () => {
|
|||||||
expect(event.defaultPrevented).toBe(true);
|
expect(event.defaultPrevented).toBe(true);
|
||||||
expect(card.classList.contains('drag-over')).toBe(false);
|
expect(card.classList.contains('drag-over')).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('ignores drops tagged as internal card drags (move-to-folder)', () => {
|
||||||
|
const card = createCard();
|
||||||
|
const file = new File(['data'], 'preview.png', { type: 'image/png' });
|
||||||
|
|
||||||
|
const event = dispatchDrop(card, [file], [MODEL_CARD_DRAG_MIME_TYPE]);
|
||||||
|
|
||||||
|
expect(uploadPreviewMock).not.toHaveBeenCalled();
|
||||||
|
expect(showToastMock).not.toHaveBeenCalled();
|
||||||
|
expect(event.defaultPrevented).toBe(false);
|
||||||
|
expect(card.classList.contains('drag-over')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not highlight or intercept internal card drags during dragover', () => {
|
||||||
|
const card = createCard();
|
||||||
|
|
||||||
|
const dragOverEvent = new Event('dragover', { bubbles: true, cancelable: true });
|
||||||
|
Object.defineProperty(dragOverEvent, 'dataTransfer', {
|
||||||
|
value: { types: [MODEL_CARD_DRAG_MIME_TYPE] },
|
||||||
|
});
|
||||||
|
card.dispatchEvent(dragOverEvent);
|
||||||
|
|
||||||
|
expect(dragOverEvent.defaultPrevented).toBe(false);
|
||||||
|
expect(card.classList.contains('drag-over')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps the card draggable (move-to-folder) but the preview image non-draggable', () => {
|
||||||
|
const card = createCard();
|
||||||
|
|
||||||
|
// The card itself must stay draggable for sidebar move-to-folder drags.
|
||||||
|
expect(card.draggable).toBe(true);
|
||||||
|
// The preview image must not start a native image drag: the browser would
|
||||||
|
// synthesize a File payload from it, which the drop handler would mistake
|
||||||
|
// for an external preview replacement.
|
||||||
|
const img = card.querySelector('.card-preview img');
|
||||||
|
expect(img.getAttribute('draggable')).toBe('false');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user