multiple-selection.js 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. // 多选框选功能模块(复制自 client/js/disk/multiple-selection.js)
  2. class ResourceManagerMultipleSelection {
  3. constructor(options) {
  4. // 必需的元素
  5. this.container = options.container; // 框选容器(dropZone)
  6. this.itemsContainer = options.itemsContainer; // 文件项容器(fileList)
  7. this.selectionBox = options.selectionBox; // 选框元素
  8. this.selectionBar = options.selectionBar; // 选择操作栏
  9. this.selectionCount = options.selectionCount; // 选择计数元素
  10. // 选择项的选择器
  11. this.itemSelector = options.itemSelector || '.file-item';
  12. // 回调函数
  13. this.onSelectionChange = options.onSelectionChange || null;
  14. // 状态
  15. this.selectedItems = new Set();
  16. this.isSelecting = false;
  17. this.selectionStart = null;
  18. this.selectionHasMoved = false;
  19. this.selectionDragJustFinished = false;
  20. this.init();
  21. }
  22. init() {
  23. this.bindEvents();
  24. }
  25. bindEvents() {
  26. // 框选事件
  27. this.container.addEventListener('mousedown', (e) => this.startSelection(e));
  28. document.addEventListener('mousemove', (e) => this.updateSelection(e));
  29. document.addEventListener('mouseup', () => this.endSelection());
  30. // 窗口失去焦点时结束框选
  31. window.addEventListener('blur', () => this.endSelection());
  32. // 拖拽结束时也结束框选
  33. document.addEventListener('dragend', () => this.endSelection());
  34. // 点击空白处取消选择
  35. this.container.addEventListener('click', (e) => this.handleContainerClick(e));
  36. }
  37. // 框选开始
  38. startSelection(e) {
  39. // 只响应左键
  40. if (e.button !== 0) return;
  41. // 如果点击的是文件项或其子元素,不启动框选
  42. if (e.target.closest(this.itemSelector)) {
  43. return;
  44. }
  45. // 如果点击的是重命名输入框,不启动框选
  46. if (e.target.classList.contains('rename-input')) {
  47. return;
  48. }
  49. // 如果有输入框正在获得焦点(重命名中),不阻止默认行为,让 blur 触发
  50. const activeInput = document.querySelector('.rename-input:focus');
  51. if (activeInput) {
  52. return;
  53. }
  54. // 阻止默认行为,防止浏览器的文本选择和拖拽干扰框选
  55. e.preventDefault();
  56. // 如果没有按 Ctrl,先清除之前的选择(Windows 行为)
  57. if (!e.ctrlKey) {
  58. this.clearSelection();
  59. }
  60. this.isSelecting = true;
  61. this.selectionHasMoved = false;
  62. this.selectionStart = { x: e.clientX, y: e.clientY };
  63. this.selectionBox.style.display = 'block';
  64. this.selectionBox.style.left = e.clientX + 'px';
  65. this.selectionBox.style.top = e.clientY + 'px';
  66. this.selectionBox.style.width = '0';
  67. this.selectionBox.style.height = '0';
  68. // 防止拖拽时选中文本
  69. document.body.style.userSelect = 'none';
  70. }
  71. // 框选更新
  72. updateSelection(e) {
  73. if (!this.isSelecting || !this.selectionStart) return;
  74. // 阻止默认行为
  75. e.preventDefault();
  76. const x = Math.min(e.clientX, this.selectionStart.x);
  77. const y = Math.min(e.clientY, this.selectionStart.y);
  78. const width = Math.abs(e.clientX - this.selectionStart.x);
  79. const height = Math.abs(e.clientY - this.selectionStart.y);
  80. // 检测是否有实际移动(超过5像素算作拖拽)
  81. if (width > 5 || height > 5) {
  82. this.selectionHasMoved = true;
  83. }
  84. this.selectionBox.style.left = x + 'px';
  85. this.selectionBox.style.top = y + 'px';
  86. this.selectionBox.style.width = width + 'px';
  87. this.selectionBox.style.height = height + 'px';
  88. // 检测框选区域内的文件
  89. const boxRect = {
  90. left: x,
  91. top: y,
  92. right: x + width,
  93. bottom: y + height
  94. };
  95. const items = this.itemsContainer.querySelectorAll(this.itemSelector);
  96. items.forEach(item => {
  97. const itemRect = item.getBoundingClientRect();
  98. const isIntersecting = !(
  99. itemRect.right < boxRect.left ||
  100. itemRect.left > boxRect.right ||
  101. itemRect.bottom < boxRect.top ||
  102. itemRect.top > boxRect.bottom
  103. );
  104. if (isIntersecting) {
  105. this.selectItem(item, false);
  106. } else if (!e.ctrlKey) {
  107. this.deselectItem(item, false);
  108. }
  109. });
  110. this.updateSelectionBar();
  111. this.triggerSelectionChange();
  112. }
  113. // 框选结束
  114. endSelection() {
  115. // 只有实际拖拽移动过才标记,防止 click 事件清除选择
  116. if (this.isSelecting && this.selectionHasMoved) {
  117. this.selectionDragJustFinished = true;
  118. }
  119. this.isSelecting = false;
  120. this.selectionStart = null;
  121. this.selectionHasMoved = false;
  122. this.selectionBox.style.display = 'none';
  123. // 恢复文本选择
  124. document.body.style.userSelect = '';
  125. }
  126. // 处理容器点击
  127. handleContainerClick(e) {
  128. // 如果刚完成拖拽框选,不清除选择
  129. if (this.selectionDragJustFinished) {
  130. this.selectionDragJustFinished = false;
  131. return;
  132. }
  133. // 如果点击的是重命名输入框,不处理
  134. if (e.target.classList.contains('rename-input')) {
  135. return;
  136. }
  137. // 如果点击的不是文件项,清除选择
  138. if (!e.target.closest(this.itemSelector)) {
  139. this.clearSelection();
  140. }
  141. }
  142. // 选中项目
  143. selectItem(item, updateBar = true) {
  144. const path = item.dataset.path;
  145. if (!this.selectedItems.has(path)) {
  146. this.selectedItems.add(path);
  147. item.classList.add('selected');
  148. if (updateBar) {
  149. this.updateSelectionBar();
  150. this.triggerSelectionChange();
  151. }
  152. }
  153. }
  154. // 取消选中项目
  155. deselectItem(item, updateBar = true) {
  156. const path = item.dataset.path;
  157. if (this.selectedItems.has(path)) {
  158. this.selectedItems.delete(path);
  159. item.classList.remove('selected');
  160. if (updateBar) {
  161. this.updateSelectionBar();
  162. this.triggerSelectionChange();
  163. }
  164. }
  165. }
  166. // 切换选中状态(Ctrl+点击)
  167. toggleSelection(item) {
  168. const path = item.dataset.path;
  169. if (this.selectedItems.has(path)) {
  170. this.deselectItem(item);
  171. } else {
  172. this.selectItem(item);
  173. }
  174. }
  175. // 只选中一个项目,取消其他选择(Windows 单击行为)
  176. selectOnly(item) {
  177. // 先清除所有选择(但不触发回调)
  178. this.itemsContainer.querySelectorAll(`${this.itemSelector}.selected`).forEach(el => {
  179. el.classList.remove('selected');
  180. });
  181. this.selectedItems.clear();
  182. // 选中当前项
  183. const path = item.dataset.path;
  184. this.selectedItems.add(path);
  185. item.classList.add('selected');
  186. this.updateSelectionBar();
  187. this.triggerSelectionChange();
  188. }
  189. // 清除所有选择
  190. clearSelection() {
  191. this.selectedItems.clear();
  192. this.itemsContainer.querySelectorAll(`${this.itemSelector}.selected`).forEach(item => {
  193. item.classList.remove('selected');
  194. });
  195. this.updateSelectionBar();
  196. this.triggerSelectionChange();
  197. }
  198. // 更新选择操作栏
  199. updateSelectionBar() {
  200. const count = this.selectedItems.size;
  201. if (this.selectionBar) {
  202. if (count > 0) {
  203. this.selectionBar.classList.add('show');
  204. if (this.selectionCount) {
  205. this.selectionCount.textContent = `已选择 ${count} 项`;
  206. }
  207. } else {
  208. this.selectionBar.classList.remove('show');
  209. }
  210. }
  211. }
  212. // 触发选择变化回调
  213. triggerSelectionChange() {
  214. if (this.onSelectionChange) {
  215. this.onSelectionChange(this.getSelectedItems());
  216. }
  217. }
  218. // 获取已选择的项目路径
  219. getSelectedItems() {
  220. return Array.from(this.selectedItems);
  221. }
  222. // 获取已选择的项目数量
  223. getSelectedCount() {
  224. return this.selectedItems.size;
  225. }
  226. // 检查是否有选中项
  227. hasSelection() {
  228. return this.selectedItems.size > 0;
  229. }
  230. // 检查指定项是否被选中
  231. isSelected(path) {
  232. return this.selectedItems.has(path);
  233. }
  234. // 全选所有项目
  235. selectAll() {
  236. const items = this.itemsContainer.querySelectorAll(this.itemSelector);
  237. items.forEach(item => {
  238. const path = item.dataset.path;
  239. this.selectedItems.add(path);
  240. item.classList.add('selected');
  241. });
  242. this.updateSelectionBar();
  243. this.triggerSelectionChange();
  244. }
  245. }