feat(selection): implement marquee selection for bulk operations

This commit is contained in:
Will Miao
2025-09-05 11:24:10 +08:00
parent 7475de366b
commit 3250fa89cb
11 changed files with 294 additions and 344 deletions

View File

@@ -318,14 +318,13 @@
"bulkOperations": {
"selected": "{count} ausgewählt",
"selectedSuffix": "ausgewählt",
"viewSelected": "Klicken Sie, um ausgewählte Elemente anzuzeigen",
"addTags": "Tags hinzufügen",
"sendToWorkflow": "An Workflow senden",
"copyAll": "Alle kopieren",
"refreshAll": "Alle aktualisieren",
"moveAll": "Alle verschieben",
"deleteAll": "Alle löschen",
"clear": "Leeren"
"viewSelected": "Auswahl anzeigen",
"addTags": "Allen Tags hinzufügen",
"copyAll": "Alle Syntax kopieren",
"refreshAll": "Alle Metadaten aktualisieren",
"moveAll": "Alle in Ordner verschieben",
"deleteAll": "Alle Modelle löschen",
"clear": "Auswahl löschen"
},
"contextMenu": {
"refreshMetadata": "Civitai-Daten aktualisieren",
@@ -983,6 +982,7 @@
"deleteFailed": "Fehler: {error}",
"deleteFailedGeneral": "Fehler beim Löschen der Modelle",
"selectedAdditional": "{count} zusätzliche {type}(s) ausgewählt",
"marqueeSelectionComplete": "{count} {type}(s) mit Rahmenauswahl ausgewählt",
"refreshMetadataFailed": "Fehler beim Aktualisieren der Metadaten",
"nameCannotBeEmpty": "Modellname darf nicht leer sein",
"nameUpdatedSuccessfully": "Modellname erfolgreich aktualisiert",

View File

@@ -318,14 +318,13 @@
"bulkOperations": {
"selected": "{count} selected",
"selectedSuffix": "selected",
"viewSelected": "Click to view selected items",
"addTags": "Add Tags",
"sendToWorkflow": "Send to Workflow",
"copyAll": "Copy All",
"refreshAll": "Refresh All",
"moveAll": "Move All",
"deleteAll": "Delete All",
"clear": "Clear"
"viewSelected": "View Selected",
"addTags": "Add Tags to All",
"copyAll": "Copy All Syntax",
"refreshAll": "Refresh All Metadata",
"moveAll": "Move All to Folder",
"deleteAll": "Delete All Models",
"clear": "Clear Selection"
},
"contextMenu": {
"refreshMetadata": "Refresh Civitai Data",
@@ -983,6 +982,7 @@
"deleteFailed": "Error: {error}",
"deleteFailedGeneral": "Failed to delete models",
"selectedAdditional": "Selected {count} additional {type}(s)",
"marqueeSelectionComplete": "Selected {count} {type}(s) with marquee selection",
"refreshMetadataFailed": "Failed to refresh metadata",
"nameCannotBeEmpty": "Model name cannot be empty",
"nameUpdatedSuccessfully": "Model name updated successfully",

View File

@@ -318,14 +318,13 @@
"bulkOperations": {
"selected": "{count} seleccionados",
"selectedSuffix": "seleccionados",
"viewSelected": "Clic para ver elementos seleccionados",
"addTags": "Añadir etiquetas",
"sendToWorkflow": "Enviar al flujo de trabajo",
"copyAll": "Copiar todo",
"refreshAll": "Actualizar todo",
"moveAll": "Mover todo",
"deleteAll": "Eliminar todo",
"clear": "Limpiar"
"viewSelected": "Ver seleccionados",
"addTags": "Añadir etiquetas a todos",
"copyAll": "Copiar toda la sintaxis",
"refreshAll": "Actualizar todos los metadatos",
"moveAll": "Mover todos a carpeta",
"deleteAll": "Eliminar todos los modelos",
"clear": "Limpiar selección"
},
"contextMenu": {
"refreshMetadata": "Actualizar datos de Civitai",
@@ -983,6 +982,7 @@
"deleteFailed": "Error: {error}",
"deleteFailedGeneral": "Error al eliminar modelos",
"selectedAdditional": "Seleccionados {count} {type}(s) adicionales",
"marqueeSelectionComplete": "Seleccionados {count} {type}(s) con selección de marco",
"refreshMetadataFailed": "Error al actualizar metadatos",
"nameCannotBeEmpty": "El nombre del modelo no puede estar vacío",
"nameUpdatedSuccessfully": "Nombre del modelo actualizado exitosamente",

View File

@@ -318,14 +318,13 @@
"bulkOperations": {
"selected": "{count} sélectionné(s)",
"selectedSuffix": "sélectionné(s)",
"viewSelected": "Cliquez pour voir les éléments sélectionnés",
"addTags": "Ajouter des tags",
"sendToWorkflow": "Envoyer vers le workflow",
"copyAll": "Tout copier",
"refreshAll": "Tout actualiser",
"moveAll": "Tout déplacer",
"deleteAll": "Tout supprimer",
"clear": "Effacer"
"viewSelected": "Voir la sélection",
"addTags": "Ajouter des tags à tous",
"copyAll": "Copier toute la syntaxe",
"refreshAll": "Actualiser toutes les métadonnées",
"moveAll": "Déplacer tout vers un dossier",
"deleteAll": "Supprimer tous les modèles",
"clear": "Effacer la sélection"
},
"contextMenu": {
"refreshMetadata": "Actualiser les données Civitai",
@@ -983,6 +982,7 @@
"deleteFailed": "Erreur : {error}",
"deleteFailedGeneral": "Échec de la suppression des modèles",
"selectedAdditional": "{count} {type}(s) supplémentaire(s) sélectionné(s)",
"marqueeSelectionComplete": "{count} {type}(s) sélectionné(s) avec la sélection par glisser-déposer",
"refreshMetadataFailed": "Échec de l'actualisation des métadonnées",
"nameCannotBeEmpty": "Le nom du modèle ne peut pas être vide",
"nameUpdatedSuccessfully": "Nom du modèle mis à jour avec succès",

View File

@@ -318,14 +318,13 @@
"bulkOperations": {
"selected": "{count} 選択中",
"selectedSuffix": "選択中",
"viewSelected": "選択したアイテムを表示するにはクリック",
"addTags": "タグを追加",
"sendToWorkflow": "ワークフローに送信",
"copyAll": "すべてコピー",
"refreshAll": "すべて更新",
"moveAll": "すべて移動",
"deleteAll": "すべて削除",
"clear": "クリア"
"viewSelected": "選択中を表示",
"addTags": "すべてにタグを追加",
"copyAll": "すべての構文をコピー",
"refreshAll": "すべてのメタデータを更新",
"moveAll": "すべてをフォルダに移動",
"deleteAll": "すべてのモデルを削除",
"clear": "選択をクリア"
},
"contextMenu": {
"refreshMetadata": "Civitaiデータを更新",
@@ -983,6 +982,7 @@
"deleteFailed": "エラー:{error}",
"deleteFailedGeneral": "モデルの削除に失敗しました",
"selectedAdditional": "{count} 追加{type}が選択されました",
"marqueeSelectionComplete": "マーキー選択で {count} の{type}が選択されました",
"refreshMetadataFailed": "メタデータの更新に失敗しました",
"nameCannotBeEmpty": "モデル名を空にすることはできません",
"nameUpdatedSuccessfully": "モデル名が正常に更新されました",

View File

@@ -318,14 +318,13 @@
"bulkOperations": {
"selected": "{count}개 선택됨",
"selectedSuffix": "개 선택됨",
"viewSelected": "선택 항목 보기",
"addTags": "태그 추가",
"sendToWorkflow": "워크플로로 전송",
"copyAll": "모두 복사",
"refreshAll": "모두 새로고침",
"moveAll": "모두 이동",
"deleteAll": "모두 삭제",
"clear": "지우기"
"viewSelected": "선택 항목 보기",
"addTags": "모두에 태그 추가",
"copyAll": "모든 문법 복사",
"refreshAll": "모든 메타데이터 새로고침",
"moveAll": "모두 폴더로 이동",
"deleteAll": "모든 모델 삭제",
"clear": "선택 지우기"
},
"contextMenu": {
"refreshMetadata": "Civitai 데이터 새로고침",
@@ -983,6 +982,7 @@
"deleteFailed": "오류: {error}",
"deleteFailedGeneral": "모델 삭제에 실패했습니다",
"selectedAdditional": "추가로 {count}개의 {type}이(가) 선택되었습니다",
"marqueeSelectionComplete": "마키 선택으로 {count}개의 {type}이(가) 선택되었습니다",
"refreshMetadataFailed": "메타데이터 새로고침에 실패했습니다",
"nameCannotBeEmpty": "모델 이름은 비어있을 수 없습니다",
"nameUpdatedSuccessfully": "모델 이름이 성공적으로 업데이트되었습니다",

View File

@@ -318,14 +318,13 @@
"bulkOperations": {
"selected": "Выбрано {count}",
"selectedSuffix": "выбрано",
"viewSelected": "Нажмите для просмотра выбранных элементов",
"addTags": "Добавить теги",
"sendToWorkflow": "Отправить в Workflow",
"copyAll": "Копировать все",
"refreshAll": "Обновить все",
"moveAll": "Переместить все",
"deleteAll": "Удалить все",
"clear": "Очистить"
"viewSelected": "Просмотреть выбранные",
"addTags": "Добавить теги ко всем",
"copyAll": "Копировать весь синтаксис",
"refreshAll": "Обновить все метаданные",
"moveAll": "Переместить все в папку",
"deleteAll": "Удалить все модели",
"clear": "Очистить выбор"
},
"contextMenu": {
"refreshMetadata": "Обновить данные Civitai",
@@ -983,6 +982,7 @@
"deleteFailed": "Ошибка: {error}",
"deleteFailedGeneral": "Не удалось удалить модели",
"selectedAdditional": "Выбрано дополнительно {count} {type}(ей)",
"marqueeSelectionComplete": "Выбрано {count} {type} с помощью выделения рамкой",
"refreshMetadataFailed": "Не удалось обновить метаданные",
"nameCannotBeEmpty": "Название модели не может быть пустым",
"nameUpdatedSuccessfully": "Название модели успешно обновлено",

View File

@@ -318,14 +318,13 @@
"bulkOperations": {
"selected": "已选中 {count} 项",
"selectedSuffix": "已选中",
"viewSelected": "点击查看已选项目",
"addTags": "批量添加标签",
"sendToWorkflow": "发送到工作流",
"copyAll": "全部复制",
"refreshAll": "全部刷新",
"moveAll": "全部移动",
"deleteAll": "全部删除",
"clear": "清除"
"viewSelected": "查看已选",
"addTags": "为所有添加标签",
"copyAll": "复制全部语法",
"refreshAll": "刷新全部元数据",
"moveAll": "全部移动到文件夹",
"deleteAll": "删除所有模型",
"clear": "清除选择"
},
"contextMenu": {
"refreshMetadata": "刷新 Civitai 数据",
@@ -983,6 +982,7 @@
"deleteFailed": "错误:{error}",
"deleteFailedGeneral": "删除模型失败",
"selectedAdditional": "已选中 {count} 个额外 {type}",
"marqueeSelectionComplete": "框选已选中 {count} 个 {type}",
"refreshMetadataFailed": "刷新元数据失败",
"nameCannotBeEmpty": "模型名称不能为空",
"nameUpdatedSuccessfully": "模型名称更新成功",

View File

@@ -318,14 +318,13 @@
"bulkOperations": {
"selected": "已選擇 {count} 項",
"selectedSuffix": "已選擇",
"viewSelected": "點擊檢視已選項目",
"addTags": "新增標籤",
"sendToWorkflow": "傳送到工作流",
"copyAll": "全部複製",
"refreshAll": "全部刷新",
"moveAll": "全部移動",
"deleteAll": "全部刪除",
"clear": "清除"
"viewSelected": "檢視已選",
"addTags": "新增標籤到全部",
"copyAll": "複製全部語法",
"refreshAll": "刷新全部 metadata",
"moveAll": "全部移動到資料夾",
"deleteAll": "刪除全部模型",
"clear": "清除選取"
},
"contextMenu": {
"refreshMetadata": "刷新 Civitai 資料",
@@ -983,6 +982,7 @@
"deleteFailed": "錯誤:{error}",
"deleteFailedGeneral": "刪除模型失敗",
"selectedAdditional": "已選擇 {count} 個額外 {type}",
"marqueeSelectionComplete": "框選已選擇 {count} 個 {type}",
"refreshMetadataFailed": "刷新 metadata 失敗",
"nameCannotBeEmpty": "模型名稱不可為空",
"nameUpdatedSuccessfully": "模型名稱已成功更新",

View File

@@ -1,81 +1,3 @@
/* Bulk Operations Styles */
.bulk-operations-panel {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateY(100px) translateX(-50%);
background: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: var(--border-radius-base);
padding: 12px 16px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
z-index: var(--z-overlay);
display: flex;
flex-direction: column;
min-width: 420px;
max-width: 900px;
width: auto;
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
opacity: 0;
}
.bulk-operations-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
gap: 20px; /* Increase space between count and buttons */
}
#selectedCount {
font-weight: 500;
background: var(--bg-color);
padding: 6px 12px;
border-radius: var(--border-radius-xs);
border: 1px solid var(--border-color);
min-width: 80px;
text-align: center;
}
.bulk-operations-actions {
display: flex;
gap: 8px;
}
.bulk-operations-actions button {
padding: 6px 12px;
border-radius: var(--border-radius-xs);
background: var(--bg-color);
border: 1px solid var(--border-color);
color: var(--text-color);
cursor: pointer;
font-size: 14px;
white-space: nowrap;
min-height: 36px;
display: flex;
align-items: center;
gap: 6px;
transition: all 0.2s ease;
}
.bulk-operations-actions button:hover {
background: var(--lora-accent);
color: white;
border-color: var(--lora-accent);
}
/* Danger button style - updated to use proper theme variables */
.bulk-operations-actions button.danger-btn {
background: oklch(70% 0.2 29); /* Light red background that works in both themes */
color: oklch(98% 0.01 0); /* Almost white text for good contrast */
border-color: var(--lora-error);
}
.bulk-operations-actions button.danger-btn:hover {
background: var(--lora-error);
color: oklch(100% 0 0); /* Pure white text on hover for maximum contrast */
}
/* Style for selected cards */
.model-card.selected {
box-shadow: 0 0 0 2px var(--lora-accent);
@@ -99,203 +21,29 @@
z-index: 1;
}
/* Update bulk operations button to match others when active */
#bulkOperationsBtn.active {
background: var(--lora-accent);
color: white;
border-color: var(--lora-accent);
}
@media (max-width: 768px) {
.bulk-operations-panel {
width: calc(100% - 40px);
min-width: unset;
max-width: unset;
left: 20px;
transform: none;
border-radius: var(--border-radius-sm);
}
.bulk-operations-actions {
flex-wrap: wrap;
}
}
.bulk-operations-panel.visible {
transform: translateY(0) translateX(-50%);
opacity: 1;
}
/* Thumbnail Strip Styles */
.selected-thumbnails-strip {
/* Marquee selection styles */
.marquee-selection {
position: fixed;
bottom: 80px; /* Position above the bulk operations panel */
left: 50%;
transform: translateX(-50%) translateY(20px);
background: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: var(--border-radius-base);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
z-index: calc(var(--z-overlay) - 1); /* Just below the bulk panel z-index */
padding: 16px;
max-width: 80%;
width: auto;
transition: all 0.3s ease;
opacity: 0;
overflow: hidden;
border: 2px dashed var(--lora-accent, #007bff);
background: rgba(0, 123, 255, 0.1);
pointer-events: none;
z-index: 9999;
border-radius: 2px;
}
.selected-thumbnails-strip.visible {
opacity: 1;
transform: translateX(-50%) translateY(0);
/* Visual feedback when marquee selecting */
.marquee-selecting {
cursor: crosshair;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
.thumbnails-container {
display: flex;
gap: 12px;
overflow-x: auto;
padding-bottom: 8px; /* Space for scrollbar */
max-width: 100%;
align-items: flex-start;
}
.selected-thumbnail {
position: relative;
width: 80px;
min-width: 80px; /* Prevent shrinking */
border-radius: var(--border-radius-xs);
border: 1px solid var(--border-color);
overflow: hidden;
cursor: pointer;
background: var(--bg-color);
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.selected-thumbnail:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.selected-thumbnail img,
.selected-thumbnail video {
width: 100%;
aspect-ratio: 1 / 1;
object-fit: cover;
display: block;
}
.thumbnail-name {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: rgba(0, 0, 0, 0.6);
color: white;
font-size: 10px;
padding: 3px 5px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.thumbnail-remove {
position: absolute;
top: 3px;
right: 3px;
width: 18px;
height: 18px;
border-radius: 50%;
background: rgba(0, 0, 0, 0.5);
color: white;
border: none;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 10px;
opacity: 0.7;
transition: opacity 0.2s ease, background-color 0.2s ease;
}
.thumbnail-remove:hover {
opacity: 1;
background: var(--lora-error);
}
.strip-close-btn {
position: absolute;
top: 5px;
right: 5px;
width: 20px;
height: 20px;
background: none;
border: none;
color: var(--text-color);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
opacity: 0.7;
transition: opacity 0.2s ease;
}
.strip-close-btn:hover {
opacity: 1;
}
/* Style the selectedCount to indicate it's clickable */
.selectable-count {
display: flex;
align-items: center;
gap: 5px;
cursor: pointer;
transition: background-color 0.2s ease;
}
.selectable-count:hover {
background: var(--lora-border);
}
.dropdown-caret {
font-size: 12px;
visibility: hidden; /* Will be shown via JS when items are selected */
}
/* Scrollbar styling for the thumbnails container */
.thumbnails-container::-webkit-scrollbar {
height: 6px;
}
.thumbnails-container::-webkit-scrollbar-track {
background: var(--bg-color);
border-radius: 3px;
}
.thumbnails-container::-webkit-scrollbar-thumb {
background: var(--border-color);
border-radius: 3px;
}
.thumbnails-container::-webkit-scrollbar-thumb:hover {
background: var(--lora-accent);
}
/* Mobile optimizations */
@media (max-width: 768px) {
.selected-thumbnails-strip {
width: calc(100% - 40px);
max-width: none;
left: 20px;
transform: translateY(20px);
border-radius: var(--border-radius-sm);
}
.selected-thumbnails-strip.visible {
transform: translateY(0);
}
.selected-thumbnail {
width: 70px;
min-width: 70px;
}
/* Prevent text selection during marquee */
.marquee-selecting * {
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}

View File

@@ -12,6 +12,12 @@ export class BulkManager {
// Remove bulk panel references since we're using context menu now
this.bulkContextMenu = null; // Will be set by core initialization
// Marquee selection properties
this.isMarqueeActive = false;
this.marqueeStart = { x: 0, y: 0 };
this.marqueeElement = null;
this.initialSelectedModels = new Set();
// Model type specific action configurations
this.actionConfig = {
[MODEL_TYPES.LORA]: {
@@ -44,6 +50,7 @@ export class BulkManager {
initialize() {
this.setupEventListeners();
this.setupGlobalKeyboardListeners();
this.setupMarqueeSelection();
}
setBulkContextMenu(bulkContextMenu) {
@@ -734,6 +741,201 @@ export class BulkManager {
}
}
}
/**
* Setup marquee selection functionality
*/
setupMarqueeSelection() {
const container = document.querySelector('.models-container') || document.body;
container.addEventListener('mousedown', (e) => {
// Disable marquee if any modal is open
if (modalManager.isAnyModalOpen()) {
return;
}
// Only start marquee selection on left click in empty areas
if (e.button !== 0 || e.target.closest('.model-card') || e.target.closest('button') || e.target.closest('input')) {
return;
}
// Prevent text selection during marquee
e.preventDefault();
this.startMarqueeSelection(e);
});
document.addEventListener('mousemove', (e) => {
// Disable marquee update if any modal is open
if (modalManager.isAnyModalOpen()) {
return;
}
if (this.isMarqueeActive) {
this.updateMarqueeSelection(e);
}
});
document.addEventListener('mouseup', (e) => {
if (this.isMarqueeActive) {
this.endMarqueeSelection(e);
}
});
// Prevent context menu during marquee selection
document.addEventListener('contextmenu', (e) => {
if (this.isMarqueeActive) {
e.preventDefault();
}
});
}
/**
* Start marquee selection
*/
startMarqueeSelection(e) {
// Store initial mouse position
this.marqueeStart.x = e.clientX;
this.marqueeStart.y = e.clientY;
// Store initial selection state
this.initialSelectedModels = new Set(state.selectedModels);
// Enter bulk mode if not already active
if (!state.bulkMode) {
this.toggleBulkMode();
}
// Create marquee element
this.createMarqueeElement();
this.isMarqueeActive = true;
// Add visual feedback class to body
document.body.classList.add('marquee-selecting');
}
/**
* Create the visual marquee selection rectangle
*/
createMarqueeElement() {
this.marqueeElement = document.createElement('div');
this.marqueeElement.className = 'marquee-selection';
this.marqueeElement.style.cssText = `
position: fixed;
border: 2px dashed var(--lora-accent, #007bff);
background: rgba(0, 123, 255, 0.1);
pointer-events: none;
z-index: 9999;
left: ${this.marqueeStart.x}px;
top: ${this.marqueeStart.y}px;
width: 0;
height: 0;
`;
document.body.appendChild(this.marqueeElement);
}
/**
* Update marquee selection rectangle and selected items
*/
updateMarqueeSelection(e) {
if (!this.marqueeElement) return;
const currentX = e.clientX;
const currentY = e.clientY;
// Calculate rectangle bounds
const left = Math.min(this.marqueeStart.x, currentX);
const top = Math.min(this.marqueeStart.y, currentY);
const width = Math.abs(currentX - this.marqueeStart.x);
const height = Math.abs(currentY - this.marqueeStart.y);
// Update marquee element position and size
this.marqueeElement.style.left = left + 'px';
this.marqueeElement.style.top = top + 'px';
this.marqueeElement.style.width = width + 'px';
this.marqueeElement.style.height = height + 'px';
// Check which cards intersect with marquee
this.updateCardSelection(left, top, left + width, top + height);
}
/**
* Update card selection based on marquee bounds
*/
updateCardSelection(left, top, right, bottom) {
const cards = document.querySelectorAll('.model-card');
const newSelection = new Set(this.initialSelectedModels);
cards.forEach(card => {
const rect = card.getBoundingClientRect();
// Check if card intersects with marquee rectangle
const intersects = !(rect.right < left ||
rect.left > right ||
rect.bottom < top ||
rect.top > bottom);
const filepath = card.dataset.filepath;
if (intersects) {
// Add to selection if intersecting
newSelection.add(filepath);
card.classList.add('selected');
// Cache metadata if not already cached
const metadataCache = this.getMetadataCache();
if (!metadataCache.has(filepath)) {
metadataCache.set(filepath, {
fileName: card.dataset.file_name,
usageTips: card.dataset.usage_tips,
previewUrl: this.getCardPreviewUrl(card),
isVideo: this.isCardPreviewVideo(card),
modelName: card.dataset.name
});
}
} else if (!this.initialSelectedModels.has(filepath)) {
// Remove from selection if not intersecting and wasn't initially selected
newSelection.delete(filepath);
card.classList.remove('selected');
}
});
// Update global selection state
state.selectedModels = newSelection;
// Update context menu header if visible
if (this.bulkContextMenu) {
this.bulkContextMenu.updateSelectedCountHeader();
}
}
/**
* End marquee selection
*/
endMarqueeSelection(e) {
this.isMarqueeActive = false;
// Remove marquee element
if (this.marqueeElement) {
this.marqueeElement.remove();
this.marqueeElement = null;
}
// Remove visual feedback class
document.body.classList.remove('marquee-selecting');
// Show toast with selection count if any items were selected
const selectionCount = state.selectedModels.size;
if (selectionCount > 0) {
const currentConfig = MODEL_CONFIG[state.currentPageType];
showToast('toast.models.marqueeSelectionComplete', {
count: selectionCount,
type: currentConfig.displayName.toLowerCase()
}, 'success');
}
// Clear initial selection state
this.initialSelectedModels.clear();
}
}
export const bulkManager = new BulkManager();