import * as d3 from 'd3'; import { TextAnalysisAPI } from '../api/GLTR_API'; import { showMoveDialog } from './folderOperations'; import { showAlertDialog } from './dialog'; // 国际化 import { tr } from '../lang/i18n-lite'; export type DemoItem = { type: 'folder' | 'file'; name: string; path: string; }; export type MultiSelectOptions = { container: d3.Selection; api: TextAnalysisAPI; getItemFullPath: (item: DemoItem) => string | null; getCurrentPath: () => string; setLoading: (loading: boolean) => void; // 实际是 setListLoading,保持参数名兼容 fetchDemoList: () => Promise; showToast: (message: string, type: 'success' | 'error') => void; showAlertDialog: (title: string, message: string) => void; onModeChange: () => void; // 当模式切换时,需要重新渲染列表 initialMultiSelectMode?: boolean; // 初始多选模式状态,默认为 false disableModeToggle?: boolean; // 禁用模式切换按钮(只读选择场景),默认为 false onSelectionChange?: (selectedCount: number) => void; // 选择数量变化时的回调 }; export type MultiSelect = { isMultiSelectMode: () => boolean; isItemSelected: (item: DemoItem) => boolean; shouldShowCheckbox: () => boolean; syncSelectionFromCheckbox: (item: DemoItem, checkboxNode: HTMLInputElement) => void; syncCheckboxFromSelection: () => void; selectAllItems: (items: DemoItem[]) => void; clearSelection: () => void; toggleMode: () => void; updateBar: () => void; initUI: (navWrapper: d3.Selection) => void; getSelectedPaths: () => string[]; // 获取所有选中项的路径 }; export function createMultiSelect(options: MultiSelectOptions): MultiSelect { const { container, api, getItemFullPath, getCurrentPath, setLoading, fetchDemoList, showToast, showAlertDialog, onModeChange, initialMultiSelectMode = false, disableModeToggle = false, onSelectionChange, } = options; // 多选模式状态(使用配置的初始值) let multiSelectMode: boolean = initialMultiSelectMode; const selectedDemos: string[] = []; // 存储选中项的完整路径(按选择顺序) // UI元素 let multiSelectBar: d3.Selection | null = null; let multiSelectToggleBtn: d3.Selection | null = null; let multiSelectCount: d3.Selection | null = null; let navWrapper: d3.Selection | null = null; // 从复选框同步状态到 selectedDemos const syncSelectionFromCheckbox = (item: DemoItem, checkboxNode: HTMLInputElement) => { // 如果复选框被禁用,不更新选择状态 if (checkboxNode.disabled) return; const itemPath = getItemFullPath(item); if (itemPath === null) return; // 直接从复选框读取状态,同步到 selectedDemos if (checkboxNode.checked) { selectedDemos.push(itemPath); } else { const index = selectedDemos.indexOf(itemPath); if (index !== -1) { selectedDemos.splice(index, 1); } } updateBar(); }; // 同步 selectedDemos 到复选框状态(用于全选/清空操作) const syncCheckboxFromSelection = () => { const demoItems = container.selectAll('.demo-item'); demoItems.each(function(d) { const demoItem = d3.select(this); const checkbox = demoItem.select('.demo-checkbox-inline'); if (!checkbox.empty()) { const itemPath = getItemFullPath(d); const shouldBeChecked = itemPath !== null && selectedDemos.includes(itemPath); const checkboxNode = checkbox.node(); if (checkboxNode) { checkboxNode.checked = shouldBeChecked; } } }); }; const selectAllItems = (items: DemoItem[]) => { items.forEach(item => { const itemPath = getItemFullPath(item); if (itemPath !== null) { selectedDemos.push(itemPath); } }); syncCheckboxFromSelection(); // 同步到复选框 updateBar(); }; const clearSelection = () => { selectedDemos.length = 0; syncCheckboxFromSelection(); // 同步到复选框 updateBar(); }; const toggleMode = () => { multiSelectMode = !multiSelectMode; // 退出多选模式时清空选择状态 if (!multiSelectMode) { clearSelection(); } updateBar(); updateToggleBtn(); onModeChange(); // 通知主模块重新渲染列表 }; const updateBar = () => { if (!multiSelectBar || !multiSelectCount) return; if (multiSelectMode) { multiSelectBar.style('display', 'flex'); const selectedCount = selectedDemos.length; // 基于checkbox原生状态判断全选按钮是否可用 // 获取当前目录下所有未禁用的checkbox(未禁用的都是文件类型) const selectableCheckboxes: HTMLInputElement[] = []; container.selectAll('.demo-checkbox-inline').each(function() { const checkbox = this; if (!checkbox.disabled) { selectableCheckboxes.push(checkbox); } }); // 检查是否所有可选择的checkbox都已选中 const allSelected = selectableCheckboxes.length > 0 && selectableCheckboxes.every(checkbox => checkbox.checked); const hasUnselected = !allSelected; // 有未选中项时全选按钮可用 multiSelectCount.text(selectedCount > 0 ? tr('Selected {count}').replace('{count}', String(selectedCount)) : tr('No selection')); // 更新所有按钮的可用状态 multiSelectBar.selectAll('.refresh-btn').each(function() { const btn = d3.select(this); const action = btn.attr('data-action'); let isActive = false; if (action === 'select-all') { isActive = hasUnselected; // 有未选中项时可用 } else if (action === 'clear' || action === 'delete' || action === 'move') { isActive = selectedCount > 0; // 有选中项时可用 } btn.classed('inactive', !isActive); }); // 通知外部选择数量已变化(用于更新弹窗确定按钮等) if (onSelectionChange) { onSelectionChange(selectedCount); } } else { multiSelectBar.style('display', 'none'); } }; const updateToggleBtn = () => { if (multiSelectToggleBtn) { if (multiSelectMode) { // 多选模式:显示选中图标 multiSelectToggleBtn .attr('title', tr('Exit multi-select mode')) .text('☑'); } else { // 非多选模式:显示未选中图标 multiSelectToggleBtn .attr('title', tr('Multi-select mode')) .text('☐'); } } }; const handleBatchDelete = async () => { const selectedItems: DemoItem[] = []; const currentPath = getCurrentPath(); const result = await api.list_demos(currentPath); const allItems = result.items || []; allItems.forEach((item: DemoItem) => { const itemPath = getItemFullPath(item); if (itemPath !== null && selectedDemos.includes(itemPath)) { selectedItems.push(item); } }); if (selectedItems.length === 0) { showAlertDialog(tr('Info'), tr('Please select items to delete first')); return; } const itemNames = selectedItems.map(item => item.name).join('\n'); const confirmMessage = tr('Are you sure you want to delete the following {count} items?').replace('{count}', String(selectedItems.length)) + '\n\n' + itemNames; if (!confirm(confirmMessage)) { return; } try { setLoading(true); let successCount = 0; let failCount = 0; const errors: string[] = []; for (const item of selectedItems) { try { let result; if (item.type === 'file') { result = await api.delete_demo(item.path); } else { result = await api.delete_folder(item.path); } if (result.success) { successCount++; } else { failCount++; errors.push(`${item.name}: ${tr(result.message || 'Delete failed')}`); } } catch (err) { failCount++; errors.push(`${item.name}: ${err instanceof Error ? tr(err.message) : tr('Delete failed')}`); } } // 刷新列表 await fetchDemoList(); // 清空选择 clearSelection(); // 显示结果 if (failCount === 0) { showToast(tr('Successfully deleted {count} items').replace('{count}', String(successCount)), 'success'); } else { const errorMsg = errors.length > 0 ? `\n\n${tr('Failed items:')}\n${errors.slice(0, 5).join('\n')}${errors.length > 5 ? `\n${tr('... and {count} more items failed').replace('{count}', String(errors.length - 5))}` : ''}` : ''; const message = tr('Successfully deleted {success} items, failed {fail} items') .replace('{success}', String(successCount)) .replace('{fail}', String(failCount)) + errorMsg; showAlertDialog(tr('Partial success'), message); } } catch (err) { console.error('批量删除失败:', err); showAlertDialog(tr('Error'), tr('Batch delete failed, please check console for details.')); } finally { setLoading(false); } }; const handleBatchMove = async () => { const selectedItems: DemoItem[] = []; const currentPath = getCurrentPath(); const result = await api.list_demos(currentPath); const allItems = result.items || []; allItems.forEach((item: DemoItem) => { const itemPath = getItemFullPath(item); if (itemPath !== null && selectedDemos.includes(itemPath)) { selectedItems.push(item); } }); if (selectedItems.length === 0) { showAlertDialog(tr('Info'), tr('Please select items to move first')); return; } try { setLoading(true); const foldersResult = await api.list_all_folders(); const folders = foldersResult.folders || []; // 排除所有选中项所在的路径及其子路径 const excludePaths = new Set(); selectedItems.forEach(item => { const excludePath = item.path; if (excludePath) { excludePaths.add(excludePath); } }); const filteredFolders = folders.filter(f => { if (excludePaths.has(f)) return false; for (const excludePath of excludePaths) { if (f.startsWith(excludePath + '/')) return false; } return true; }); showMoveDialog(filteredFolders, currentPath, async (targetPath: string) => { try { setLoading(true); let successCount = 0; let failCount = 0; const errors: string[] = []; for (const item of selectedItems) { try { let result; if (item.type === 'file') { result = await api.move_demo(item.path, targetPath); } else { result = await api.move_folder(item.path, targetPath); } if (result.success) { successCount++; } else { failCount++; errors.push(`${item.name}: ${tr(result.message || 'Move failed')}`); } } catch (err) { failCount++; errors.push(`${item.name}: ${err instanceof Error ? tr(err.message) : tr('Move failed')}`); } } // 刷新列表 await fetchDemoList(); // 清空选择 clearSelection(); // 显示结果 if (failCount === 0) { showToast(tr('Successfully moved {count} items').replace('{count}', String(successCount)), 'success'); } else { const errorMsg = errors.length > 0 ? `\n\n${tr('Failed items:')}\n${errors.slice(0, 5).join('\n')}${errors.length > 5 ? `\n${tr('... and {count} more items failed').replace('{count}', String(errors.length - 5))}` : ''}` : ''; const message = tr('Successfully moved {success} items, failed {fail} items') .replace('{success}', String(successCount)) .replace('{fail}', String(failCount)) + errorMsg; showAlertDialog(tr('Partial success'), message); } } catch (err) { console.error('批量移动失败:', err); showAlertDialog(tr('Error'), tr('Batch move failed, please check console for details.')); } finally { setLoading(false); } }); } catch (err) { console.error('获取文件夹列表失败:', err); showAlertDialog(tr('Error'), tr('Failed to get folder list, please check console for details.')); } finally { setLoading(false); } }; // 初始化UI const initUI = ( navWrapperParam: d3.Selection ) => { // 保存navWrapper引用,用于宽度检测 navWrapper = navWrapperParam; // 在内部查找 createFolderBtn const createFolderBtn = navWrapper.select(`button[title="${tr('New folder')}"]`); // 创建多选模式控制栏 // 如果有新建文件夹按钮,则在它之前插入;否则直接放在 navWrapper 末尾 if (!createFolderBtn.empty()) { const createFolderBtnNode = createFolderBtn.node(); if (createFolderBtnNode && createFolderBtnNode instanceof HTMLElement) { multiSelectBar = d3.select(createFolderBtnNode.parentElement) .insert('div', () => createFolderBtnNode) .attr('class', 'demo-multiselect-bar-center') .style('display', 'none') .style('flex-shrink', '0'); } } else { multiSelectBar = navWrapper.append('div') .attr('class', 'demo-multiselect-bar-center') .style('display', 'none') .style('flex-shrink', '0'); } // 在多选控制栏的右边添加多选切换按钮(如果未禁用) if (!disableModeToggle && !createFolderBtn.empty()) { const createFolderBtnNode = createFolderBtn.node(); if (createFolderBtnNode && createFolderBtnNode instanceof HTMLElement) { multiSelectToggleBtn = d3.select(createFolderBtnNode.parentElement) .insert('button', () => createFolderBtnNode) .attr('class', 'refresh-btn') .attr('title', tr('Multi-select mode')) .text('☐') // 初始状态:未选中 .style('flex-shrink', '0') .on('click', toggleMode); // 初始化按钮状态 updateToggleBtn(); } } // 初始化多选模式控制栏(按钮样式与新建文件夹按钮一致) if (multiSelectBar) { // 不再在控制栏中显示退出按钮,使用固定的切换按钮 multiSelectCount = multiSelectBar.append('span') .attr('class', 'multiselect-count') .style('font-size', '9pt') .style('color', 'var(--text-muted)') .style('margin-left', '0px') .style('margin-right', '6px') // 减少30%:8px → 6px .text(tr('No selection')); multiSelectBar.append('button') .attr('class', 'refresh-btn') .attr('data-action', 'select-all') .attr('title', tr('Select all')) .text(tr('Select all')) .on('click', () => { // 获取所有demo项,但只选择未被禁用的项 const allItems: DemoItem[] = []; container.selectAll('.demo-item').each(function(d) { const demoItem = d3.select(this); const checkbox = demoItem.select('.demo-checkbox-inline'); if (!checkbox.empty()) { const checkboxNode = checkbox.node(); // 只添加未被禁用的项 if (checkboxNode && !checkboxNode.disabled) { allItems.push(d); } } }); selectAllItems(allItems); }); multiSelectBar.append('button') .attr('class', 'refresh-btn') .attr('data-action', 'clear') .attr('title', tr('Clear')) .text(tr('Clear')) .on('click', clearSelection); // 只在非只读模式下显示删除和移动按钮 if (!disableModeToggle) { multiSelectBar.append('button') .attr('class', 'refresh-btn') .attr('data-action', 'delete') .attr('title', tr('Delete')) .text(tr('Delete')) .on('click', handleBatchDelete); multiSelectBar.append('button') .attr('class', 'refresh-btn') .attr('data-action', 'move') .attr('title', tr('Move')) .text(tr('Move')) .on('click', handleBatchMove); } // 初始化控制栏显示状态(在 forceMultiSelect 模式下需要显示) updateBar(); } }; const getSelectedPaths = (): string[] => { return [...selectedDemos]; }; return { isMultiSelectMode: () => multiSelectMode, isItemSelected: (item: DemoItem) => { const itemPath = getItemFullPath(item); return itemPath !== null && selectedDemos.includes(itemPath); }, shouldShowCheckbox: () => multiSelectMode, syncSelectionFromCheckbox, syncCheckboxFromSelection, selectAllItems, clearSelection, toggleMode, updateBar, initUI, getSelectedPaths, }; }