import { onBeforeUnmount, onMounted, ref, Ref } from 'vue'; import { IColumnConfig, IDraggingData, IEmits, IProps } from "../types" import throttle from "lodash/throttle" interface IUseDragSortParams { props: IProps emit: IEmits viewSettingDragSortOptions: Ref beforeDragStart: () => boolean currScope: Ref pageSize: Ref tableDomRef: Ref } export function useDragSort({ props, emit, viewSettingDragSortOptions, pageSize, beforeDragStart, currScope, tableDomRef }: IUseDragSortParams) { const draggingData = ref({}); // 拖拽相关数据 const isMouseDown = ref(false); const dragType = ref<'row' | 'view-setting'>('row'); // 如果页面存在多个table实例,如果没有这个值来区分正在操作哪个表格,拖拽功能会有异常 const isOperating = ref(false); onMounted(() => { tableDomRef.value.$el.addEventListener('mousedown', () => { isOperating.value = true }); document.addEventListener('mousedown', handleDocumentMouseDown); document.addEventListener('mousemove', handleDocumentMouseMove); document.addEventListener('mouseup', handleDocumentMouseUp); }) onBeforeUnmount(() => { tableDomRef.value.$el.removeEventListener('mousedown', () => isOperating.value = true); document.removeEventListener('mousedown', handleDocumentMouseDown); document.removeEventListener('mousemove', handleDocumentMouseMove); document.removeEventListener('mouseup', handleDocumentMouseUp); }) const handleDocumentMouseDown = (e) => { isMouseDown.value = true; const target = e.target as HTMLElement if (typeof target.dataset?.index === 'undefined') return if ([...target.classList].includes('row-drag-target')) { dragType.value = 'row'; if (!isOperating.value) return // 处理列表行拖拽 handleDragMouseDown(e, +target.dataset.index); } if ([...target.classList].includes('editable-table-view-setting-drag-target')) { dragType.value = 'view-setting'; // 处理显示设置拖拽 handleViewSettingDragMouseDown(e, +target.dataset.index); } } const handleDocumentMouseMove = (e) => { if (!isMouseDown.value || typeof draggingData.value.draggingIndex === 'undefined') { return; } throttledMoveHandler(e); } const handleDocumentMouseUp = (e) => { isOperating.value = false; isMouseDown.value = false; if (typeof draggingData.value.draggingIndex === 'undefined') { return; } if (dragType.value === 'row') { handleDragDrop(e, currScope.value); } if (dragType.value === 'view-setting') { handleViewSettingDragDrop(); } } const handleDragMouseDown = (event, draggingIndex) => { if (!beforeDragStart()) return const rows = [...document.getElementsByClassName(`custom-row-classname`)] as HTMLElement[]; const rowDoms: Record = []; for (let i = 0; i < rows.length; i++) { const row = rows[i]; const clsIndex = ([...row.classList].find(cls => cls.startsWith(`custom-row-classname-`)) ?? '').split('-').pop() ?? 'NaN'; if (typeof +clsIndex === 'number') { rowDoms[+clsIndex] = rowDoms[+clsIndex] || []; rowDoms[+clsIndex].push(row); } } draggingData.value.isDragging = true; draggingData.value.rowDoms = rowDoms; draggingData.value.draggingIndex = +draggingIndex; draggingData.value.rowsRange = []; draggingData.value.startPosition = { x: event.clientX, y: event.clientY }; // 列表过大时拖拽有性能问题,所以拖拽范围限制在拖拽元素的上下15个元素内 const dragSemiRange = props.dragSemiRange ?? 15; for (const i of Object.keys(rowDoms).map(Number)) { if (i < draggingIndex - dragSemiRange || i > draggingIndex + dragSemiRange) { draggingData.value.rowsRange[i] = undefined continue } const rowdom = rowDoms[i]; // 在Chrom中,relative加z-index可以让拖拽更流畅,然而在Safari中无效 // 如果在Safari中relative有效,其实蒙层都没有必要了 rowdom.forEach(el => el.style.setProperty('position', 'relative')); if (i !== draggingData.value.draggingIndex) { rowdom.forEach(el => el.style.setProperty('transition', 'transform 0.25s ease')); } else { rowdom.forEach(el => el.style.setProperty('z-index', '100')); // 当有固定列时,一行的内容并不是一个tr,而是两个或多个tr的叠加显示,其中有的列内容被隐藏 // 所以拖拽时,需要同时拖拽所有的tr,并显示所有内容,否则拖拽效果会显示异常 const tds = rowdom .map(dom => dom.querySelectorAll('td')) .reduce((pre, curr) => ([...pre, ...curr]), [] as HTMLElement[]) .filter(td => td.classList.contains('is-hidden')); draggingData.value.hiddenTds = tds; tds.forEach(td => td.classList.remove('is-hidden')); // 生成一个蒙层,防止拖动时文字被选中 const fullScreenDiv = document.createElement('div'); fullScreenDiv.style.position = 'fixed'; fullScreenDiv.style.top = '0'; fullScreenDiv.style.left = '0'; fullScreenDiv.style.right = '0'; fullScreenDiv.style.bottom = '0'; fullScreenDiv.style.zIndex = '999'; fullScreenDiv.style.cursor = 'grabbing'; document.body.appendChild(fullScreenDiv); draggingData.value.fullScreenDiv = fullScreenDiv; } // 计算每一行的高度范围 const rect = rowdom[0].getBoundingClientRect() draggingData.value.rowsRange = draggingData.value.rowsRange || []; draggingData.value.rowsRange[i] = ([ rect.top, rect.bottom ]); } } const calcPositionRowShouldBePlace = (rowsRange: ([number, number])[], draggingClientY: number) => { if (!draggingData.value.rowsRange || typeof draggingData.value.draggingIndex === 'undefined' || typeof props.dragSemiRange === 'undefined' ) return; const rangeLeft = draggingData.value.draggingIndex - props.dragSemiRange <= 0 ? 0 : draggingData.value.draggingIndex - props.dragSemiRange const rangeRight = draggingData.value.draggingIndex + props.dragSemiRange >= draggingData.value.rowsRange.length ? draggingData.value.rowsRange.length - 1 : draggingData.value.draggingIndex + props.dragSemiRange if (!rowsRange) return -1; if (draggingClientY <= rowsRange[rangeLeft][0]) return rangeLeft; if (draggingClientY >= rowsRange[rangeRight][1]) return rangeRight; for (let i = rangeLeft; i <= rangeRight; i++) { const [top, bottom] = rowsRange[i]; if (draggingClientY > top && draggingClientY < bottom) { return i; } } return -1; } const clearMovingData = () => { const rowdoms = draggingData.value.rowDoms || []; const draggingIndex = draggingData.value.draggingIndex ?? -1 const dragSemiRange = props.dragSemiRange ?? 15; for (const i of Object.keys(rowdoms).map(Number)) { if (i < draggingIndex - dragSemiRange || i > draggingIndex + dragSemiRange) continue const doms = rowdoms[i]; doms.forEach(el => { el.style.removeProperty('position'); el.style.removeProperty('transition'); el.style.removeProperty('transform'); el.style.removeProperty('z-index'); }); } draggingData.value.fullScreenDiv?.parentNode?.removeChild(draggingData.value.fullScreenDiv); draggingData.value.hiddenTds?.forEach(el => el.classList.add('is-hidden')); draggingData.value = {}; currScope.value = null } const handleDragDrop = (event, scope) => { if (!scope) return const { draggingIndex = 0, dropIndex = draggingIndex } = draggingData.value; const dataList = scope.store.states.data; const movedRow = dataList[draggingIndex]; const newList = [ ...dataList.slice(0, +draggingIndex), ...dataList.slice(draggingIndex + 1) ]; newList.splice(dropIndex, 0, movedRow); scope.store.states.data = newList; emit('row-drag-drop', { row: movedRow, fromIndex: draggingIndex, toIndex: dropIndex, page: props.currentPage, size: pageSize.value }); // 清理工作 clearMovingData(); } const handleViewSettingDragMouseDown = (event, index) => { const rowDoms = [...document.getElementsByClassName('editable-table-view-setting-draggable-item')] .reduce((pre, item, index) => ({ ...pre, [index]: [item] }), {}); draggingData.value.isDragging = true; draggingData.value.rowDoms = rowDoms; draggingData.value.draggingIndex = +index; draggingData.value.startPosition = { x: event.clientX, y: event.clientY }; for (const i of Object.keys(rowDoms).map(Number)) { const rowdoms = rowDoms[i]; rowdoms.forEach((el) => { el.style.setProperty('position', 'relative'); }); if (i !== draggingData.value.draggingIndex) { rowdoms.forEach(el => el.style.setProperty('transition', 'transform 0.25s ease')); } const rect = rowdoms[0].getBoundingClientRect(); draggingData.value.rowsRange = draggingData.value.rowsRange || []; draggingData.value.rowsRange.push([ rect.top, rect.bottom ]); } } const handleMove = (event) => { if (!draggingData.value.isDragging) return; const { y = 0 } = draggingData.value.startPosition || {}; const { draggingIndex = -1, rowDoms = {} } = draggingData.value; const draggingRow = rowDoms?.[draggingIndex ?? 0]; for (let i = 0; i < draggingRow.length; i++) { const dom = draggingRow[i]; dom?.style.setProperty('transform', `translateY(${event.clientY - y}px)`); } // 判断应该落在第几行 const idx = calcPositionRowShouldBePlace(draggingData.value.rowsRange as [number, number][], event.clientY); if (typeof idx !== 'number') return; const [top, bottom] = draggingData.value.rowsRange![draggingIndex] as [number, number]; draggingData.value.dropIndex = idx; const rowHeight = bottom - top; const dragSemiRange = props.dragSemiRange ?? 15; for (const i of Object.keys(rowDoms).map(Number)) { if (i < draggingIndex - dragSemiRange || i > draggingIndex + dragSemiRange || draggingIndex === i) continue const doms = rowDoms[i]; if (idx < draggingIndex) { if (i >= idx && i < draggingIndex) { doms.forEach(el => el.style.setProperty('transform', `translateY(${rowHeight}px)`)); } else { doms.forEach(el => el.style.removeProperty('transform')); } } else if (idx > draggingIndex) { if (i <= idx && i > draggingIndex) { doms.forEach(el => el.style.setProperty('transform', `translateY(${-rowHeight}px)`)); } else { doms.forEach(el => el.style.removeProperty('transform')); } } else { doms.forEach(el => el.style.removeProperty('transform')); } } } const handleViewSettingDragDrop = () => { const { draggingIndex = 0, dropIndex = draggingIndex } = draggingData.value; const movedRow = viewSettingDragSortOptions.value[draggingIndex]; const newList = [ ...viewSettingDragSortOptions.value.slice(0, draggingIndex), ...viewSettingDragSortOptions.value.slice(draggingIndex + 1) ]; newList.splice(dropIndex, 0, movedRow); viewSettingDragSortOptions.value = newList; // 清理工作 clearMovingData(); } const throttledMoveHandler = throttle(handleMove, 10); }