import React, { createContext, useRef, useState, forwardRef, useContext, } from "react"; import classNames from "classnames"; import { TableColumn } from "../../TableProps"; import { getElementOffset } from "../../../_util/get-element-offset"; import { mergeRefs } from "../../../_util/merge-refs"; /** * 拖拽 Context */ interface DragContextValue { /** * 供表格内容区设置其 DOM Ref */ setBodyRef?: (element: HTMLDivElement) => void; /** * 表格body区Ref */ bodyRef?: React.RefObject; /** * 供表格body区遮罩设置其 DOM Ref */ setBodyMaskRef?: (element: HTMLDivElement) => void; /** * 表格body区遮罩Ref */ bodyMaskRef?: React.RefObject; /** * 供表格body区高亮插入线设置其 DOM Ref */ setBodyLineRef?: (element: HTMLDivElement) => void; /** * 高亮插入线Ref */ bodyLineRef?: React.RefObject; /** * 拖拽的列名称 */ draggingColumn?: string; /** * 设置拖拽列名称 */ setDraggingColumn?: React.Dispatch>; /** * 列的类名称 */ columnClassNames?: React.ComponentState; /** * 列的类名称 */ columns?: TableColumn[]; /** * 设置列的类名称 */ setColumns?: React.Dispatch>; /** * 事件句柄对象 */ dragEventHandlers?: React.DOMAttributes; /** * 可拖拽的列, 默认所有列 * @default all */ draggableColumns?: string[] | "all"; } /** * 拖拽 Context 实例 */ export const DragContext = createContext({}); DragContext.displayName = "DragContext"; /** * 注入到外层容器 */ interface DraggableTableProps { /** * 表格 */ table: JSX.Element; /** * 可拖拽的列, 默认所有列 * @default all */ draggableColumns?: string[] | "all"; /** * 拖拽完成回调 */ onDragEnd?: (columns: TableColumn[]) => void; /** * 列配置 */ columns: TableColumn[]; /** * className */ className?: string; } export function DraggableTable({ table, draggableColumns, onDragEnd, columns, ...props }: DraggableTableProps) { const [draggingColumn, setDraggingColumn] = useState(""); const [columnClassNames, setColumnClassNames] = useState({}); const [mouseStartX, setMouseStartX] = useState(0); const bodyRef = useRef(null); const bodyMaskRef = useRef(null); const bodyLineRef = useRef(null); function addColumnClassNames(column: string, className: string) { setColumnClassNames(preState => { if (preState[column] && Array.isArray(preState[column])) { const classNameSet = new Set(preState[column]); classNameSet.add(className); preState[column] = [...classNameSet]; } else { preState[column] = [className]; } return { ...preState }; }); } function deleteColumnClassNames(className: string) { setColumnClassNames(preState => { for (const column in preState) { if (preState[column] && Array.isArray(preState[column])) { const classNameSet = new Set(preState[column]); classNameSet.delete(className); preState[column] = [...classNameSet]; } else { preState[column] = []; } } return { ...preState }; }); } 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 dragImage = document.createElement("div"); dragImage.style.width = "1px"; dragImage.style.height = "1px"; dragImage.style.backgroundColor = "#ebeef2"; dragImage.style.position = "absolute"; dragImage.style.top = "-1000px"; document.body.appendChild(dragImage); if ("setDragImage" in event.dataTransfer) { event.dataTransfer.setDragImage(dragImage, 0, 0); } // 添加拖拽选择样式 const draggingColumn = event.currentTarget.attributes.getNamedItem( "data-column" ).value; setDraggingColumn(draggingColumn); addColumnClassNames(draggingColumn, "is-dragging"); // 展示遮罩层 const maskWith = event.currentTarget.clientWidth; const currentElementOffset = getElementOffset( event.currentTarget as HTMLElement ); const tableElementOffset = getElementOffset( bodyRef.current as HTMLElement ); requestAnimationFrame(() => { bodyMaskRef.current.style.display = "block"; bodyMaskRef.current.style.transform = `translate3d(${currentElementOffset.left - tableElementOffset.left}px, 0px, 0px)`; bodyMaskRef.current.style.width = `${maskWith}px`; }); setMouseStartX(event.pageX); }, onDrag: (event: React.DragEvent) => { event.stopPropagation(); const tableElementOffset = getElementOffset( bodyRef.current as HTMLElement ); const currentElementOffset = getElementOffset( event.currentTarget as HTMLElement ); const left = currentElementOffset.left - tableElementOffset.left + (event.pageX - mouseStartX); requestAnimationFrame(() => { bodyMaskRef.current.style.transform = `translate3d(${left}px, 0px, 0px)`; }); }, onDragEnd: (event: React.DragEvent) => { requestAnimationFrame(() => { bodyMaskRef.current.style.display = "none"; bodyMaskRef.current.style.transform = "translate3d(0px, 0px, 0px)"; bodyLineRef.current.style.display = "none"; bodyLineRef.current.style.transform = "translate3d(0px, 0px, 0px)"; setMouseStartX(0); deleteColumnClassNames("is-dragging"); deleteColumnClassNames("is-draggable"); setDraggingColumn(""); }); }, onDragEnter: (event: React.DragEvent) => { event.preventDefault(); }, onDragOver: (event: React.DragEvent) => { event.preventDefault(); event.dataTransfer.dropEffect = "move"; const dropColumn = event.currentTarget.attributes.getNamedItem( "data-column" ).value; const dropIndex = columns.findIndex(item => item.key === dropColumn); const dragIndex = columns.findIndex(item => item.key === draggingColumn); const dropElementOffset = getElementOffset( event.currentTarget as HTMLElement ); const tableElementOffset = getElementOffset( bodyRef.current as HTMLElement ); // 固定列/ignore列不作排序 if ( !columns[dropIndex] || (columns[dropIndex] && columns[dropIndex].fixed) ) { return; } if (dropIndex <= dragIndex) { requestAnimationFrame(() => { bodyLineRef.current.style.display = "block"; bodyLineRef.current.style.transform = `translate3d(${dropElementOffset.left - tableElementOffset.left}px, 0px, 0px)`; }); return; } bodyLineRef.current.style.display = "block"; let leftValue = dropElementOffset.left - tableElementOffset.left + event.currentTarget.clientWidth; if (leftValue > bodyRef.current.clientWidth - 2) { leftValue = bodyRef.current.clientWidth - 2; } requestAnimationFrame(() => { bodyLineRef.current.style.transform = `translate3d(${leftValue}px, 0px, 0px)`; }); }, onDragLeave: (event: React.DragEvent) => { event.preventDefault(); }, onDrop: (event: React.DragEvent) => { event.preventDefault(); if (!draggingColumn) { return; } const dropColumn = event.currentTarget.attributes.getNamedItem( "data-column" ).value; const dropIndex = columns.findIndex(item => item.key === dropColumn); const dragIndex = columns.findIndex(item => item.key === draggingColumn); if (dropIndex === dragIndex) { return; } // 固定列/ignore列不作排序 if ( !columns[dropIndex] || (columns[dropIndex] && columns[dropIndex].fixed) ) { return; } if (columns) { const newColumns = [...columns]; newColumns.splice(dropIndex, 0, newColumns.splice(dragIndex, 1)[0]); onDragEnd(newColumns); } }, onMouseEnter: (event: React.MouseEvent) => { const targetColumn = event.currentTarget.attributes.getNamedItem( "data-column" ).value; addColumnClassNames(targetColumn, "is-hover"); }, onMouseLeave: (event: React.MouseEvent) => { deleteColumnClassNames("is-hover"); }, }; const dragContext: DragContextValue = { bodyRef, setBodyRef: body => { bodyRef.current = body; }, bodyMaskRef, setBodyMaskRef: bodyMask => { bodyMaskRef.current = bodyMask; }, bodyLineRef, setBodyLineRef: bodyLine => { bodyLineRef.current = bodyLine; }, dragEventHandlers, draggableColumns, columns, draggingColumn, setDraggingColumn, columnClassNames, }; return ( {React.cloneElement(table, { className: classNames(table.props.className, props.className), // eslint-disable-line })} ); } /** * 包装表格内容区 */ interface DraggableTableBodyProps { body: React.FunctionComponentElement<{ ref: React.Ref; style: React.CSSProperties; }>; onDragEnd?: (columns: TableColumn[]) => void; draggableColumns?: string[] | "all"; } export const DraggableTableBody = forwardRef( ( { body, onDragEnd }: DraggableTableBodyProps, ref: React.Ref ) => { const bodyRef = useRef(null); const { setBodyRef, setBodyMaskRef, setBodyLineRef, bodyLineRef, columns, draggingColumn, } = useContext(DragContext); const getHoverTdNode = (event: React.MouseEvent) => { let currentTd = null; const trs: HTMLElement[] = Array.prototype.slice.call( bodyRef.current.getElementsByTagName("tr"), 0 ); const tds: HTMLElement[] = trs && trs.length > 0 ? Array.prototype.slice.call(trs[0].childNodes, 0) : []; if (tds && tds.length > 0) { const len = tds.length; for (let i = 0; i < len; i++) { const currentElementOffset = getElementOffset(tds[i]); if ( currentElementOffset.left < event.pageX && event.pageX < currentElementOffset.left + tds[i].clientWidth ) { currentTd = tds[i]; break; } } } return currentTd; }; /** * 挂在遮罩层的事件处理器 * 当鼠标移动到table的body区域的遮罩层时,需重新计算插入位置和样式 */ const maskDragEventHandlers = { onDragEnter: (event: React.DragEvent) => { event.preventDefault(); }, onDragOver: (event: React.DragEvent) => { event.preventDefault(); const currentTd = getHoverTdNode(event); const dataColumnNode = currentTd && currentTd.attributes.getNamedItem("data-column"); if (dataColumnNode) { const dropColumn = dataColumnNode.value; const dropIndex = columns.findIndex(item => item.key === dropColumn); const dropElementOffset = getElementOffset(currentTd as HTMLElement); const tableElementOffset = getElementOffset( bodyRef.current as HTMLElement ); const dragIndex = columns.findIndex( item => item.key === draggingColumn ); // 固定列/ignore列不作排序 if ( !columns[dropIndex] || (columns[dropIndex] && columns[dropIndex].fixed) ) { return; } if (dropIndex <= dragIndex) { requestAnimationFrame(() => { bodyLineRef.current.style.display = "block"; bodyLineRef.current.style.transform = `translate3d(${dropElementOffset.left - tableElementOffset.left}px, 0px, 0px)`; }); return; } bodyLineRef.current.style.display = "block"; let leftValue = dropElementOffset.left - tableElementOffset.left + currentTd.clientWidth; if (leftValue > bodyRef.current.clientWidth - 2) { leftValue = bodyRef.current.clientWidth - 2; } requestAnimationFrame(() => { bodyLineRef.current.style.transform = `translate3d(${leftValue}px, 0px, 0px)`; }); } }, onDrop: (event: React.DragEvent) => { event.preventDefault(); const currentTd = getHoverTdNode(event); const dataColumnNode = currentTd && currentTd.attributes.getNamedItem("data-column"); if (dataColumnNode) { const dropColumn = dataColumnNode.value; const dropIndex = columns.findIndex(item => item.key === dropColumn); const dragIndex = columns.findIndex( item => item.key === draggingColumn ); if (dropIndex === dragIndex) { return; } // 固定列/ignore列不作排序 if ( !columns[dropIndex] || (columns[dropIndex] && columns[dropIndex].fixed) ) { return; } if (columns) { const newColumns = [...columns]; newColumns.splice(dropIndex, 0, newColumns.splice(dragIndex, 1)[0]); onDragEnd(newColumns); } } }, }; return (
{React.cloneElement(body, { ref: mergeRefs(setBodyRef, body.ref, ref, bodyRef), })}
); } ); DraggableTable.displayName = "DraggableTable";