import React, { createContext, useRef, useState, forwardRef, useContext, } from "react"; import { getElementOffset } from "../../../_util/get-element-offset"; import { mergeRefs } from "../../../_util/merge-refs"; import { CheckTreeRelation } from "../../../checktree"; import { now } from "../../../_util/now"; import { deepCopy } from "../../../_util/deep-copy"; import { getRowKeyFromRecordKey } from "../../util/get-row-key-from-record-key"; import { useConfig } from "../../../util"; interface HoverTime { id: string; startTime: number; } interface DragInfos { dragKey?: string; dropKey?: string; depth?: number; } let hoverTimeInfos: HoverTime = { id: "", startTime: 0, }; let tdPaddingLeft: undefined | number; let insertLineHandler = 0; let dragElementWidthFromCursorToHead = 0; let dragRecord: { record?: any; index?: number } = {}; /** * 行拖拽Context */ interface RowsDragContextValue { /** * 供表格内容区设置其 DOM Ref */ setBodyRef?: (element: HTMLDivElement) => void; /** * 表格body区Ref */ bodyRef?: React.RefObject; /** * 供表格body区高亮插入线设置其 DOM Ref */ setBodyLineRef?: (element: HTMLDivElement) => void; /** * 高亮插入线Ref */ bodyLineRef?: React.RefObject; /** * 事件句柄对象 */ dragEventHandlers?: React.DOMAttributes; } /** * 拖拽 Context 实例 */ export const RowsDragContext = createContext({}); RowsDragContext.displayName = "RowsDragContext"; /** * 注入到外层容器 */ interface RowsDraggableTableProps { /** * 表格 */ table: JSX.Element; /** * 拖拽完成回调 */ onDragEnd: ( records: Record[], dragContext?: { dragKey?: string; dragRecordIndex?: number; dragRecord?: Record; dropKey?: string; dropDepth?: number; } ) => void; /** * 拖拽开始时回调 */ onDragStart?: (context: any) => void; /** * 提供 `relations` 属性,则会按照树状选择逻辑进行 */ relations?: CheckTreeRelation; /** * 用户进行展开/收起操作的时候,知会最新的键值 */ onExpandedKeysChange?: ( value: string[], context?: { event: React.SyntheticEvent; operateType: "expand" | "collapse"; operateKey: string; operateRecord: Record; } ) => void; /** * 展开行的键值 */ expandedKeys?: string[]; /** * 子级相对父级缩进量 * @default 20 */ indent?: number; /** * 数据记录 */ recordsData?: Record[]; /** * */ getRowKey: ReturnType; /** * 子树字段名 */ childrenColName?: string; /** * 拖拽类型,树拖拽排序或者仅行拖拽 */ dragType?: "tree" | "row"; /** * icon 列宽度 */ iconColumnWidth?: number; } export function RowsDraggableTable({ table, onDragEnd, onDragStart, relations, indent, onExpandedKeysChange, expandedKeys, recordsData, getRowKey, dragType, childrenColName, iconColumnWidth, ...props }: RowsDraggableTableProps) { const [draggingRowKey, setDraggingRowKey] = useState(""); const [dragInfos, setDragInfos] = useState({}); const bodyRef = useRef(null); const bodyLineRef = useRef(null); /** * 获取行的树深度 & paddingLeft * @param rowKey 行ID * @returns */ const getDepthAndPaddingLeft = (rowKey: string) => { let paddingLeft = 0; let depth = 0; if (indent > 0) { let node = rowKey; while (relations && relations[node]) { depth += 1; node = relations[node]; } paddingLeft = indent * depth; } return { paddingLeft, depth }; }; /** * 绘制插入线 */ const drawInsertLine = (width: number, left: number, top: number) => { if (insertLineHandler) { cancelAnimationFrame(insertLineHandler); } insertLineHandler = requestAnimationFrame(() => { bodyLineRef.current.style.display = "block"; bodyLineRef.current.style.width = `${width}px`; bodyLineRef.current.style.transform = `translate3d(${left + iconColumnWidth}px, ${top}px, 0px)`; bodyLineRef.current.style.pointerEvents = "none"; }); }; /** * 重建树 * @param dragKey 拖拽排序的原行key * @param baseKey 拖拽到基准目标行的key * @param depth 插入的树的深度 * @param records 表格数据 */ const rebuildTree = ( dragKey: string, baseKey: string, depth: number, records: any[] ) => { let subtree; let baseKeyIndex; let subtreeIndex; const operateSubtreeDataLoop = ( key: string, records: any[], type: "getAndRemoveSubtree" | "getBaseKeyIndex" | "addSubtree" ) => { if (!records || !Array.isArray(records)) { return; } /* eslint-disable */ for (const index in records) { const isMatch = !!( records[index] && getRowKey(records[index], parseInt(index, 10)) === key ); const isGetSubtreeAndMatch = isMatch && type === "getAndRemoveSubtree"; if (isGetSubtreeAndMatch) { subtree = records[index]; subtreeIndex = index; records.splice(parseInt(index, 10), 1); dragRecord = { record: deepCopy(subtree), index: parseInt(index, 10), }; break; } const isGetBaseKeyIndexAndMatch = isMatch && type === "getBaseKeyIndex"; if (isGetBaseKeyIndexAndMatch) { baseKeyIndex = parseInt(index, 10); break; } const isAddSubtree = isMatch && type === "addSubtree"; if ( isAddSubtree && typeof baseKeyIndex !== "undefined" && records[index] && subtree ) { if ( records[index][childrenColName] && Array.isArray(records[index][childrenColName]) ) { records[index][childrenColName].splice( parseInt(baseKeyIndex, 10), 0, subtree ); } else { records[index][childrenColName] = [subtree]; } break; } if ( records[index] && records[index][childrenColName] && Array.isArray(records[index][childrenColName]) ) { operateSubtreeDataLoop(key, records[index][childrenColName], type); } } }; // 获取baseKeyIndex if (baseKey) { operateSubtreeDataLoop(baseKey, records, "getBaseKeyIndex"); } // 获取并从原树中删除子树 operateSubtreeDataLoop(dragKey, records, "getAndRemoveSubtree"); const addSubtree = (baseKey: string, depth: number, records: any[]) => { const { depth: baseKeyDepth } = getDepthAndPaddingLeft(baseKey); const parentKey = relations && typeof relations[baseKey] !== "undefined" ? relations[baseKey] : null; // 插入首行 if (baseKey === "") { records.splice(0, 0, subtree); } if ((!baseKeyIndex && baseKeyIndex !== 0) || !subtree) { return; } // 深度为0插入根节点 if (depth === 0) { let nodeKey = baseKey; while (relations && relations[nodeKey]) { nodeKey = relations[nodeKey]; } const baseKeyParentIndex = records.findIndex( (record: any, index) => getRowKey(record, index) === nodeKey ); records.splice(baseKeyParentIndex + 1, 0, subtree); return; } // 为基准节点的兄弟节点 if (depth === baseKeyDepth) { // 判断摘除的子节点是否是和base节点是兄弟节点 if (relations[dragKey] !== relations[baseKey]) { baseKeyIndex += 1; } if ( relations[dragKey] && relations[baseKey] && relations[dragKey] === relations[baseKey] && subtreeIndex > baseKeyIndex ) { baseKeyIndex += 1; } operateSubtreeDataLoop(parentKey, records, "addSubtree"); return; } // 为基准节点的子节点 if (depth > baseKeyDepth) { baseKeyIndex = 0; operateSubtreeDataLoop(baseKey, records, "addSubtree"); return; } // 为基准节点的父节点的兄弟节点 if (depth < baseKeyDepth) { let len = baseKeyDepth - depth + 1; let nodeKey = baseKey; while (len > 0) { nodeKey = relations[nodeKey]; len -= 1; // 更新下baseKeyIndex if (len === 1) { operateSubtreeDataLoop(nodeKey, records, "getBaseKeyIndex"); baseKeyIndex += 1; } } operateSubtreeDataLoop(nodeKey, records, "addSubtree"); } }; // 添加子树 addSubtree(baseKey, depth, records); const cleanEmptyTreeNode = (records: any[]) => { for (const record of records) { if ( record[childrenColName] && Array.isArray(record[childrenColName]) && record[childrenColName].length > 0 ) { cleanEmptyTreeNode(record[childrenColName]); } if ( record[childrenColName] && Array.isArray(record[childrenColName]) && record[childrenColName].length === 0 ) { delete record[childrenColName]; } } }; // 清除空数组属性 cleanEmptyTreeNode(records); }; const rebuildRow = ( dragKey: string, baseKey: string, depth: number, records: any[] ) => { if (dragKey === baseKey) { return; } let dragKeyIndex; let baseKeyIndex; /* eslint-disable */ for (const index in records) { if ( records[index] && getRowKey(records[index], parseInt(index, 10)) === dragKey ) { dragKeyIndex = parseInt(index, 10); } if ( records[index] && getRowKey(records[index], parseInt(index, 10)) === baseKey ) { baseKeyIndex = parseInt(index, 10); } } if (!baseKey) { records.splice(0, 0, records.splice(dragKeyIndex, 1)[0]); } else { if (baseKeyIndex < dragKeyIndex) { baseKeyIndex += 1; } const row = records.splice(dragKeyIndex, 1)[0]; records.splice(baseKeyIndex, 0, row); dragRecord = { record: deepCopy(row), index: parseInt(dragKeyIndex, 10) }; } }; const dragEventHandlers = { onDragStart: (event: React.DragEvent) => { event.stopPropagation(); // QQ 浏览器设置 text 不触发 drop // event.dataTransfer.setData("text", "text"); event.dataTransfer.setData("image/png", "img"); event.dataTransfer.effectAllowed = "move"; const draggingRowKey = event.currentTarget.attributes.getNamedItem( "data-key" ).value; // 获取光标到被拖元素头部距离 const dragElementProperties = getElementOffset( event.currentTarget as HTMLElement ); dragElementWidthFromCursorToHead = event.clientX - dragElementProperties.left; dragElementWidthFromCursorToHead = dragElementWidthFromCursorToHead < 0 ? 0 : dragElementWidthFromCursorToHead; if (typeof tdPaddingLeft === "undefined") { const firstTdNode = event.currentTarget.firstChild; tdPaddingLeft = typeof window.getComputedStyle !== "undefined" ? parseInt( window .getComputedStyle(firstTdNode as Element, null) .getPropertyValue("padding-left") ) : 0; } if (dragType === "tree" && expandedKeys) { const newExpandedKeys = new Set(expandedKeys); if (newExpandedKeys.has(draggingRowKey)) { newExpandedKeys.delete(draggingRowKey); onExpandedKeysChange([...newExpandedKeys]); } } setDragInfos({}); setDraggingRowKey(draggingRowKey); onDragStart({ draggableId: draggingRowKey, mode: "FLUID", source: { droppableId: "droppable", index: recordsData.findIndex((row, index) => getRowKey(row, index) === draggingRowKey) }, type: "DEFAULT", }); }, onDrag: (event: React.DragEvent) => { event.stopPropagation(); }, onDragEnd: (event: React.DragEvent) => { if (insertLineHandler) { cancelAnimationFrame(insertLineHandler); } bodyLineRef.current.style.display = "none"; setDraggingRowKey(""); }, onDragEnter: (event: React.DragEvent) => { event.preventDefault(); if (dragType === "tree") { hoverTimeInfos = { id: event.currentTarget.attributes.getNamedItem("data-key").value, startTime: now(), }; } }, onDragOver: (event: React.DragEvent) => { event.preventDefault(); event.dataTransfer.dropEffect = "move"; const bodyElementProperties = getElementOffset(bodyRef.current); const hoverRowKey = event.currentTarget.attributes.getNamedItem( "data-key" ).value; const hoverElementProperties = getElementOffset( event.currentTarget as HTMLElement ); const hoverElementMiddleLineYValue = hoverElementProperties.top + hoverElementProperties.height / 2; const previousNode = event.currentTarget.previousElementSibling; const nextNode = event.currentTarget.nextElementSibling; let dragElementHeadClientX = event.clientX - dragElementWidthFromCursorToHead; dragElementHeadClientX = dragElementHeadClientX < 0 ? 0 : dragElementHeadClientX; let insertDepth = 0; let insertLineLeft = 0; let insertLineTop = 0; let insertLineWidth = hoverElementProperties.width; if (dragType === "row") { let dropKey = ""; // hover行是拖拽的行,无法插入 if (draggingRowKey === hoverRowKey) { bodyLineRef.current.style.display = "none"; return; } if (event.clientY < hoverElementMiddleLineYValue && previousNode) { const previousElementProperties = getElementOffset( previousNode as HTMLElement ); dropKey = previousNode.attributes.getNamedItem("data-key").value; insertLineTop = previousElementProperties.top - bodyElementProperties.top + previousElementProperties.height - 1; } if (event.clientY >= hoverElementMiddleLineYValue) { dropKey = hoverRowKey; insertLineTop = hoverElementProperties.top - bodyElementProperties.top + hoverElementProperties.height - 1; } drawInsertLine(insertLineWidth, 0, insertLineTop); setDragInfos({ depth: 0, dragKey: draggingRowKey, dropKey, }); return; } // 如果是关闭情况需要执行展开操作 const parentKeys = []; /* eslint-disable */ for (const key in relations) { parentKeys.push(relations[key]); } if (hoverRowKey === hoverTimeInfos.id) { const spendTime = now() - hoverTimeInfos.startTime; if ( !expandedKeys.includes(hoverRowKey) && parentKeys.includes(hoverRowKey) && spendTime >= 1000 && hoverRowKey !== draggingRowKey ) { const newExpandedKeys = new Set(expandedKeys); newExpandedKeys.add(hoverRowKey); onExpandedKeysChange([...newExpandedKeys]); } } else { hoverTimeInfos.id = hoverRowKey; hoverTimeInfos.startTime = now(); } // 插入线绘制 const { paddingLeft: hoverElementPaddingLeft, depth: hoverElementDepth, } = getDepthAndPaddingLeft(hoverRowKey); // 插入hover元素上方 if (event.clientY < hoverElementMiddleLineYValue) { if (!previousNode) { insertLineLeft = hoverElementPaddingLeft; insertLineTop = hoverElementProperties.top - bodyElementProperties.top; insertDepth = 0; drawInsertLine(insertLineWidth, insertLineLeft, insertLineTop); setDragInfos({ depth: insertDepth, dragKey: draggingRowKey, dropKey: "", }); return; } const previousRowKey = previousNode.attributes.getNamedItem("data-key") .value; const { paddingLeft: previousElementPaddingLeft, depth: previousElementDepth, } = getDepthAndPaddingLeft(previousRowKey); const previousElementProperties = getElementOffset( previousNode as HTMLElement ); const previousElementInsertBoundaryXValue = previousElementProperties.left + previousElementPaddingLeft + indent * 3; if (draggingRowKey === previousRowKey) { bodyLineRef.current.style.display = "none"; return; } // 上层元素是父级情况,只能插入其子级 if (previousElementDepth < hoverElementDepth) { insertLineLeft = previousElementProperties.left - bodyElementProperties.left + previousElementPaddingLeft + indent + tdPaddingLeft; insertLineTop = previousElementProperties.top - bodyElementProperties.top + previousElementProperties.height - 1; insertDepth = previousElementDepth + 1; insertLineWidth -= insertLineLeft; drawInsertLine(insertLineWidth, insertLineLeft, insertLineTop); setDragInfos({ depth: insertDepth, dragKey: draggingRowKey, dropKey: previousRowKey, }); return; } // 上层是同级,可以插入其同级或子级 if ( dragElementHeadClientX > previousElementInsertBoundaryXValue && previousElementDepth === hoverElementDepth ) { insertLineLeft = previousElementProperties.left - bodyElementProperties.left + previousElementPaddingLeft + indent + tdPaddingLeft; insertLineTop = previousElementProperties.top - bodyElementProperties.top + previousElementProperties.height - 1; insertDepth = previousElementDepth + 1; } else if (previousElementDepth > hoverElementDepth) { // 上层非子级,当深度大于hover行,最终插入深度不能浅于hover行 const distance = previousElementInsertBoundaryXValue - dragElementHeadClientX; const substractDepth = Math.floor(distance / indent); insertDepth = previousElementDepth - substractDepth; insertDepth = insertDepth < hoverElementDepth ? hoverElementDepth : insertDepth; insertDepth = insertDepth > previousElementDepth ? previousElementDepth + 1 : insertDepth; insertLineLeft = previousElementProperties.left - bodyElementProperties.left + insertDepth * indent + tdPaddingLeft; insertLineTop = previousElementProperties.top - bodyElementProperties.top + previousElementProperties.height - 1; } else { insertLineLeft = previousElementProperties.left - bodyElementProperties.left + previousElementPaddingLeft + tdPaddingLeft; insertLineTop = previousElementProperties.top - bodyElementProperties.top + previousElementProperties.height - 1; insertDepth = previousElementDepth; } insertLineWidth -= insertLineLeft; drawInsertLine(insertLineWidth, insertLineLeft, insertLineTop); setDragInfos({ depth: insertDepth, dragKey: draggingRowKey, dropKey: previousRowKey, }); return; } // 插入hover元素下方 // hover行是拖拽的行,且为第一级(深度为0)不能操作 if (draggingRowKey === hoverRowKey && hoverElementDepth === 0) { bodyLineRef.current.style.display = "none"; return; } // 判断插入层级的clientX临界点 const hoverElementInsertBoundaryXValue = hoverElementProperties.left + hoverElementPaddingLeft + indent * 3; // hover行是拖拽的行,此处可以插入其父级 if ( (draggingRowKey === hoverRowKey && hoverElementDepth > 0) || !nextNode ) { const distance = hoverElementInsertBoundaryXValue - dragElementHeadClientX; const substractDepth = Math.floor(distance / indent); insertDepth = hoverElementDepth - substractDepth; insertDepth = insertDepth < 0 ? 0 : insertDepth; insertDepth = insertDepth > hoverElementDepth ? !nextNode && draggingRowKey !== hoverRowKey ? hoverElementDepth + 1 : hoverElementDepth : insertDepth; insertLineLeft = hoverElementProperties.left - bodyElementProperties.left + insertDepth * indent + tdPaddingLeft; insertLineTop = hoverElementProperties.top - bodyElementProperties.top + hoverElementProperties.height - 1; insertLineWidth -= insertLineLeft; drawInsertLine(insertLineWidth, insertLineLeft, insertLineTop); setDragInfos({ depth: insertDepth, dragKey: draggingRowKey, dropKey: hoverRowKey, }); return; } const nextRowKey = nextNode.attributes.getNamedItem("data-key").value; const { depth: nextElementDepth } = getDepthAndPaddingLeft(nextRowKey); // hover行的下一个是子级,只能插入hover行后子级 if (hoverElementDepth < nextElementDepth) { insertLineLeft = hoverElementProperties.left - bodyElementProperties.left + hoverElementPaddingLeft + indent + tdPaddingLeft; insertLineTop = hoverElementProperties.top - bodyElementProperties.top + hoverElementProperties.height - 1; insertDepth = hoverElementDepth + 1; insertLineWidth -= insertLineLeft; drawInsertLine(insertLineWidth, insertLineLeft, insertLineTop); setDragInfos({ depth: insertDepth, dragKey: draggingRowKey, dropKey: hoverRowKey, }); return; } // hover行的下一个是同级或大级,插入hover行后不能超过下一级深度 if (dragElementHeadClientX > hoverElementInsertBoundaryXValue) { insertLineLeft = hoverElementProperties.left - bodyElementProperties.left + (hoverElementDepth + 1) * indent + tdPaddingLeft; insertLineTop = hoverElementProperties.top - bodyElementProperties.top + hoverElementProperties.height - 1; insertDepth = hoverElementDepth + 1; } else { const distance = hoverElementInsertBoundaryXValue - dragElementHeadClientX; const substractDepth = Math.floor(distance / indent); insertDepth = hoverElementDepth - substractDepth; insertDepth = insertDepth < nextElementDepth ? nextElementDepth : insertDepth; insertLineLeft = hoverElementProperties.left - bodyElementProperties.left + insertDepth * indent + tdPaddingLeft; insertLineTop = hoverElementProperties.top - bodyElementProperties.top + hoverElementProperties.height - 1; } insertLineWidth -= insertLineLeft; drawInsertLine(insertLineWidth, insertLineLeft, insertLineTop); setDragInfos({ depth: insertDepth, dragKey: draggingRowKey, dropKey: hoverRowKey, }); }, onDragLeave: (event: React.DragEvent) => { event.preventDefault(); }, onDrop: (event: React.DragEvent) => { event.preventDefault(); if (!draggingRowKey) { return; } const records = deepCopy(recordsData); if (dragInfos.dragKey && dragType === "tree") { rebuildTree( dragInfos.dragKey, dragInfos.dropKey, dragInfos.depth, records ); } if (dragInfos.dragKey && dragType === "row") { rebuildRow( dragInfos.dragKey, dragInfos.dropKey, dragInfos.depth, records ); } if (insertLineHandler) { cancelAnimationFrame(insertLineHandler); } bodyLineRef.current.style.display = "none"; onDragEnd(records, { dragKey: dragInfos.dragKey, dragRecordIndex: dragRecord.index, dragRecord: dragRecord.record, dropKey: dragInfos.dropKey, dropDepth: dragInfos.depth, }); }, }; const dragContext: RowsDragContextValue = { bodyRef, setBodyRef: body => { bodyRef.current = body; }, bodyLineRef, setBodyLineRef: bodyLine => { bodyLineRef.current = bodyLine; }, dragEventHandlers, }; return ( {React.cloneElement(table, { ...props, })} ); } interface DraggableTableRowProps { row: JSX.Element; dataKey: string; draggableRowKeys?: string[] | "all"; } export const DraggableTableRow = forwardRef( ( { row, dataKey, draggableRowKeys, ...props }: DraggableTableRowProps, ref: React.Ref ) => { const { dragEventHandlers } = useContext(RowsDragContext); let eventHandlers = {}; let isDraggable = true; if ( draggableRowKeys === "all" || (draggableRowKeys && Array.isArray(draggableRowKeys) && draggableRowKeys.includes(dataKey)) ) { eventHandlers = dragEventHandlers; } else { isDraggable = false; } return React.cloneElement(row, { ...props, ...eventHandlers, ref, draggable: isDraggable, "data-key": dataKey, }); } ); /** * 包装表格内容区 */ interface RowsDraggableTableBodyProps { body: React.FunctionComponentElement<{ ref: React.Ref; style: React.CSSProperties; children: JSX.Element; }>; } export const RowsDraggableTableBody = forwardRef( ( { body, ...props }: RowsDraggableTableBodyProps, ref: React.Ref ) => { const { classPrefix } = useConfig(); const { setBodyRef, setBodyLineRef } = useContext(RowsDragContext); return (
{React.cloneElement(body, { ...props, ref: mergeRefs(setBodyRef, body.ref, ref), })}
); } ); RowsDraggableTable.displayName = "RowsDraggableTable";