fix(ModelTags): fix performance and UX issues in ModelTags

This commit is contained in:
Will Miao
2025-10-12 22:31:10 +08:00
parent bddf023dc4
commit 01bbaa31a8
2 changed files with 215 additions and 59 deletions

View File

@@ -92,6 +92,39 @@
border-radius: var(--border-radius-xs);
padding: 4px 8px;
position: relative;
cursor: grab;
transition: transform 0.18s ease;
}
.metadata-item:active {
cursor: grabbing;
}
.metadata-item-dragging {
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.25);
cursor: grabbing;
opacity: 0.95;
transition: none;
}
.metadata-item-placeholder {
border: 1px dashed var(--lora-accent);
border-radius: var(--border-radius-xs);
background: rgba(255, 255, 255, 0.1);
pointer-events: none;
}
.metadata-items-sorting .metadata-item {
transition: transform 0.18s ease;
}
body.metadata-drag-active {
user-select: none;
cursor: grabbing;
}
body.metadata-drag-active * {
cursor: grabbing !important;
}
.metadata-item-content {

View File

@@ -19,11 +19,15 @@ const MODEL_TYPE_SUGGESTION_KEY_MAP = {
const METADATA_ITEM_SELECTOR = '.metadata-item';
const METADATA_ITEMS_CONTAINER_SELECTOR = '.metadata-items';
const METADATA_ITEM_DRAGGING_CLASS = 'metadata-item-dragging';
const METADATA_ITEM_PLACEHOLDER_CLASS = 'metadata-item-placeholder';
const METADATA_ITEMS_SORTING_CLASS = 'metadata-items-sorting';
const BODY_DRAGGING_CLASS = 'metadata-drag-active';
let activeModelTypeKey = '';
let priorityTagSuggestions = [];
let priorityTagSuggestionsLoaded = false;
let priorityTagSuggestionsPromise = null;
let activeTagDragState = null;
function normalizeModelTypeKey(modelType) {
if (!modelType) {
@@ -496,87 +500,202 @@ function setupTagDragAndDrop() {
return;
}
if (!container._dragEventsBound) {
container.addEventListener('dragover', handleTagContainerDragOver);
container.addEventListener('drop', handleTagContainerDrop);
container._dragEventsBound = true;
}
container.querySelectorAll(METADATA_ITEM_SELECTOR).forEach((item) => {
if (item.dataset.dragInit === 'true') {
item.removeAttribute('draggable');
if (item.classList.contains(METADATA_ITEM_PLACEHOLDER_CLASS)) {
return;
}
item.setAttribute('draggable', 'true');
item.addEventListener('dragstart', handleTagDragStart);
item.addEventListener('dragend', handleTagDragEnd);
item.dataset.dragInit = 'true';
if (item.dataset.pointerDragInit === 'true') {
return;
}
item.addEventListener('pointerdown', handleTagPointerDown);
item.dataset.pointerDragInit = 'true';
});
}
function handleTagDragStart(event) {
const item = event.currentTarget;
if (!item) {
function handleTagPointerDown(event) {
if (event.button !== 0) {
return;
}
if (event.target.closest('.metadata-delete-btn')) {
return;
}
const item = event.currentTarget;
const container = item?.closest(METADATA_ITEMS_CONTAINER_SELECTOR);
if (!item || !container) {
return;
}
event.preventDefault();
startPointerDrag({ item, container, startEvent: event });
}
function startPointerDrag({ item, container, startEvent }) {
if (activeTagDragState) {
finishPointerDrag();
}
const itemRect = item.getBoundingClientRect();
const placeholder = document.createElement('div');
placeholder.className = `metadata-item ${METADATA_ITEM_PLACEHOLDER_CLASS}`;
placeholder.style.width = `${itemRect.width}px`;
placeholder.style.height = `${itemRect.height}px`;
container.insertBefore(placeholder, item);
item.classList.add(METADATA_ITEM_DRAGGING_CLASS);
if (event.dataTransfer) {
event.dataTransfer.effectAllowed = 'move';
try {
event.dataTransfer.setData('text/plain', item.dataset.tag || '');
} catch (error) {
// Some browsers may throw if dataTransfer is unavailable; ignore.
item.style.width = `${itemRect.width}px`;
item.style.height = `${itemRect.height}px`;
item.style.position = 'fixed';
item.style.left = `${itemRect.left}px`;
item.style.top = `${itemRect.top}px`;
item.style.pointerEvents = 'none';
item.style.zIndex = '1000';
container.classList.add(METADATA_ITEMS_SORTING_CLASS);
if (document.body) {
document.body.classList.add(BODY_DRAGGING_CLASS);
}
const dragState = {
container,
item,
placeholder,
offsetX: startEvent.clientX - itemRect.left,
offsetY: startEvent.clientY - itemRect.top,
lastKnownPointer: { x: startEvent.clientX, y: startEvent.clientY },
rafId: null,
};
activeTagDragState = dragState;
document.addEventListener('pointermove', handlePointerMove);
document.addEventListener('pointerup', handlePointerUp);
document.addEventListener('pointercancel', handlePointerUp);
}
function handlePointerMove(event) {
if (!activeTagDragState) {
return;
}
activeTagDragState.lastKnownPointer = { x: event.clientX, y: event.clientY };
if (activeTagDragState.rafId !== null) {
return;
}
activeTagDragState.rafId = requestAnimationFrame(() => {
if (!activeTagDragState) {
return;
}
}
activeTagDragState.rafId = null;
updateDraggingItemPosition();
updatePlaceholderPosition();
});
}
function handleTagDragEnd(event) {
const item = event.currentTarget;
if (!item) {
return;
}
item.classList.remove(METADATA_ITEM_DRAGGING_CLASS);
function handlePointerUp() {
finishPointerDrag();
}
function handleTagContainerDragOver(event) {
event.preventDefault();
const container = event.currentTarget;
const draggingItem = container.querySelector(`.${METADATA_ITEM_DRAGGING_CLASS}`);
if (!draggingItem) {
function updateDraggingItemPosition() {
if (!activeTagDragState) {
return;
}
const afterElement = getDragAfterElement(container, event.clientY);
if (!afterElement) {
container.appendChild(draggingItem);
const { item, offsetX, offsetY, lastKnownPointer } = activeTagDragState;
const left = lastKnownPointer.x - offsetX;
const top = lastKnownPointer.y - offsetY;
item.style.left = `${left}px`;
item.style.top = `${top}px`;
}
function updatePlaceholderPosition() {
if (!activeTagDragState) {
return;
}
if (afterElement !== draggingItem) {
container.insertBefore(draggingItem, afterElement);
}
}
const { container, placeholder, item, lastKnownPointer } = activeTagDragState;
const siblings = Array.from(
container.querySelectorAll(
`${METADATA_ITEM_SELECTOR}:not(.${METADATA_ITEM_PLACEHOLDER_CLASS})`
)
).filter((element) => element !== item);
function handleTagContainerDrop(event) {
event.preventDefault();
updateSuggestionsDropdown();
}
let insertAfter = null;
function getDragAfterElement(container, y) {
const draggableElements = Array.from(
container.querySelectorAll(`${METADATA_ITEM_SELECTOR}:not(.${METADATA_ITEM_DRAGGING_CLASS})`)
);
for (const sibling of siblings) {
const rect = sibling.getBoundingClientRect();
return draggableElements.reduce(
(closest, child) => {
const box = child.getBoundingClientRect();
const offset = y - box.top - box.height / 2;
if (offset < 0 && offset > closest.offset) {
return { offset, element: child };
if (lastKnownPointer.y < rect.top) {
container.insertBefore(placeholder, sibling);
return;
}
if (lastKnownPointer.y <= rect.bottom) {
if (lastKnownPointer.x < rect.left + rect.width / 2) {
container.insertBefore(placeholder, sibling);
return;
}
return closest;
},
{ offset: Number.NEGATIVE_INFINITY, element: null }
).element;
insertAfter = sibling;
continue;
}
insertAfter = sibling;
}
if (!insertAfter) {
container.insertBefore(placeholder, container.firstElementChild);
return;
}
container.insertBefore(placeholder, insertAfter.nextSibling);
}
function finishPointerDrag() {
if (!activeTagDragState) {
return;
}
const { container, item, placeholder, rafId } = activeTagDragState;
document.removeEventListener('pointermove', handlePointerMove);
document.removeEventListener('pointerup', handlePointerUp);
document.removeEventListener('pointercancel', handlePointerUp);
container.classList.remove(METADATA_ITEMS_SORTING_CLASS);
if (document.body) {
document.body.classList.remove(BODY_DRAGGING_CLASS);
}
if (rafId !== null) {
cancelAnimationFrame(rafId);
activeTagDragState.rafId = null;
updateDraggingItemPosition();
updatePlaceholderPosition();
}
if (placeholder && placeholder.parentNode === container) {
container.insertBefore(item, placeholder);
container.removeChild(placeholder);
}
item.classList.remove(METADATA_ITEM_DRAGGING_CLASS);
item.style.position = '';
item.style.width = '';
item.style.height = '';
item.style.left = '';
item.style.top = '';
item.style.pointerEvents = '';
item.style.zIndex = '';
activeTagDragState = null;
updateSuggestionsDropdown();
}
/**
@@ -694,8 +813,12 @@ function updateSuggestionsDropdown() {
}
function getCurrentEditTags() {
const currentTags = document.querySelectorAll('.metadata-item');
return Array.from(currentTags).map(tag => tag.dataset.tag);
const currentTags = document.querySelectorAll(
`${METADATA_ITEM_SELECTOR}[data-tag]`
);
return Array.from(currentTags)
.map(tag => tag.dataset.tag)
.filter(Boolean);
}
/**