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:
Will Miao
2026-08-18 20:22:32 +08:00
parent d2f50f26f1
commit 5d1a22fb8f
4 changed files with 61 additions and 4 deletions
+4
View File
@@ -9,6 +9,7 @@ import { bulkManager } from '../managers/BulkManager.js';
import { showToast } from '../utils/uiHelpers.js';
import { performFolderUpdateCheck } from '../utils/updateCheckHelpers.js';
import { escapeHtml, escapeAttribute } from './shared/utils.js';
import { MODEL_CARD_DRAG_MIME_TYPE } from '../utils/constants.js';
export class SidebarManager {
constructor() {
@@ -252,6 +253,9 @@ export class SidebarManager {
if (dataTransfer) {
dataTransfer.effectAllowed = 'move';
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 {
dataTransfer.setData('application/json', JSON.stringify({ filePaths }));
} catch (error) {
+13 -2
View File
@@ -3,7 +3,7 @@ import { state, getCurrentPageState } from '../../state/index.js';
import { showModelModal } from './ModelModal.js';
import { bulkManager } from '../../managers/BulkManager.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 { getModelApiClient } from '../../api/modelApiFactory.js';
import { showDeleteModal } from '../../utils/modalUtils.js';
@@ -455,6 +455,9 @@ function showExampleAccessModal(card, modelType) {
export function createModelCard(model, modelType) {
const card = document.createElement('div');
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.dataset.sha256 = model.sha256;
card.dataset.filepath = model.file_path;
@@ -647,7 +650,7 @@ export function createModelCard(model, modelType) {
<div class="card-preview ${shouldBlur ? 'blurred' : ''}">
${isVideo ?
`<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">
${shouldBlur ?
@@ -741,6 +744,11 @@ export function createModelCard(model, modelType) {
// Dropping an image/video onto the card replaces the model preview via the
// 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) => {
event.preventDefault();
event.stopPropagation();
@@ -748,17 +756,20 @@ export function createModelCard(model, modelType) {
['dragenter', 'dragover'].forEach((eventName) => {
card.addEventListener(eventName, (event) => {
if (isInternalCardDrag(event)) return;
preventDragDefaults(event);
card.classList.add('drag-over');
});
});
card.addEventListener('dragleave', (event) => {
if (isInternalCardDrag(event)) return;
preventDragDefaults(event);
card.classList.remove('drag-over');
});
card.addEventListener('drop', (event) => {
if (isInternalCardDrag(event)) return;
preventDragDefaults(event);
card.classList.remove('drag-over');