Last active
May 22, 2018 13:24
-
-
Save techird/edd4e54fc04f7ea6c7c57201011db7da to your computer and use it in GitHub Desktop.
表格拖动排序交互
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 拖动上下文,贯穿拖动排序的整个生命周期 | |
*/ | |
export interface DraggingContext { | |
startPosition: [number, number]; | |
activated: boolean; | |
source: HTMLTableRowElement; | |
sourceIndex: number; | |
sourceHeight: number; | |
sourceMidPoint: number; | |
target: HTMLTableRowElement; | |
targetIndex: number; | |
rowSeq: HTMLTableRowElement[]; | |
bottomSeq: number[]; | |
movememtSeq: number[]; | |
} | |
export type DragEventHandler = (context: Partial<DraggingContext>) => void; | |
export interface TableSortableOptions { | |
onDragStart: DragEventHandler, | |
onDragActivate: DragEventHandler, | |
onDragMove: DragEventHandler, | |
onDrop: DragEventHandler, | |
} | |
const defaultOptions: TableSortableOptions = { | |
onDragStart: () => {}, | |
onDragActivate: () => {}, | |
onDragMove: () => {}, | |
onDrop: () => {}, | |
} | |
export function tableSortable( | |
table: HTMLTableElement | HTMLTableSectionElement, | |
options: Partial<TableSortableOptions> = {}, | |
) { | |
const { onDragStart, onDragActivate, onDragMove, onDrop } = Object.assign({}, defaultOptions, options); | |
let mousedownListener = null; | |
let mousemoveListener = null; | |
let mouseupListener = null; | |
// 拖动上下文,贯穿整个拖动的声明周期 | |
let context: Partial<DraggingContext> = null; | |
// 开始拖动 | |
const start = (source: HTMLTableRowElement, startPosition: [number, number]) => { | |
context = { | |
activated: false, | |
source, | |
startPosition, | |
}; | |
onDragStart(context); | |
} | |
// 启动拖动 | |
const activate = () => { | |
const source = context.source; | |
// 来源行的高度 | |
const sourceHeight = source.getBoundingClientRect().height; | |
// 行序列 | |
const rowSeq = Array.from(table.querySelectorAll('tr')); | |
// 计算底线序列 | |
const heightSeq = rowSeq.map(tr => tr.getBoundingClientRect().height); | |
const bottomSeq = []; | |
let bottom = 0; | |
for (let height of heightSeq) { | |
bottomSeq.push(bottom += height); | |
} | |
// 来源行索引 | |
const sourceIndex = rowSeq.indexOf(source); | |
// 来源行中点位置,用于后续计算变更集 | |
const sourceMidPoint = bottomSeq[sourceIndex] + sourceHeight / 2; | |
// 更新拖动上下文 | |
Object.assign(context, { | |
activated: true, | |
sourceIndex, | |
sourceHeight, | |
sourceMidPoint, | |
rowSeq, | |
bottomSeq, | |
}); | |
onDragActivate(context); | |
} | |
// 处理拖动 | |
const move = (dy: number) => { | |
const { source, sourceIndex, sourceHeight, sourceMidPoint, rowSeq, bottomSeq, movememtSeq } = context; | |
// 对于每一个底线位置,根据当前拖动位置来决定对应的行需要移动的位移 | |
const newMovementSeq = bottomSeq.map((bottom, index) => { | |
// 来源行直接响应交互位移 | |
if (index === sourceIndex) { | |
return dy; | |
} | |
// 这个条件比较复杂 | |
// 看图:https://ask.qcloudimg.com/draft/1000002/nfp070gvoe.jpg | |
const matchDirection = () => dy * (index - sourceIndex) > 0; | |
const overMidPoint = () => dy * (sourceMidPoint + dy - bottom - (dy < 0 ? sourceHeight : 0)) > 0 | |
if (matchDirection() && overMidPoint()) { | |
return dy > 0 ? -sourceHeight : sourceHeight; | |
} | |
return 0; | |
}); | |
// 计算需要变更的行,设置样式 | |
for (let index = 0; index < newMovementSeq.length; index++) { | |
const row = rowSeq[index]; | |
const oldMovement = movememtSeq ? movememtSeq[index] : 0; | |
const newMovement = newMovementSeq[index]; | |
if (oldMovement !== newMovement) { | |
row.style.zIndex = index === sourceIndex ? '100' : '0'; | |
row.style.transition = index === sourceIndex ? 'none' : null; | |
row.style.transform = `translate3d(0, ${newMovement}px, 0)`; | |
} | |
} | |
// 记录当前移动集 | |
context.movememtSeq = newMovementSeq; | |
onDragMove(context); | |
} | |
// 拖放结束 | |
const drop = () => { | |
const { source, sourceIndex, movememtSeq, rowSeq, activated } = context; | |
let targetIndex = sourceIndex; | |
let target = source; | |
if (activated) { | |
while (targetIndex > 0 && movememtSeq[targetIndex - 1] > 0) { | |
targetIndex--; | |
} | |
if (targetIndex === sourceIndex) { | |
while (targetIndex < movememtSeq.length - 1 && movememtSeq[targetIndex + 1] < 0) { | |
targetIndex++; | |
} | |
} | |
for (let tr of Array.from(table.querySelectorAll('tr'))) { | |
tr.style.removeProperty('z-index'); | |
tr.style.removeProperty('transform'); | |
tr.style.removeProperty('transition'); | |
} | |
target = rowSeq[targetIndex]; | |
Object.assign(context, { | |
targetIndex, | |
target, | |
}); | |
onDrop(context); | |
} | |
context = null; | |
} | |
// 绑定事件 | |
const setup = () => { | |
table.addEventListener('mousedown', mousedownListener = (evt: MouseEvent) => { | |
let source = evt.target as HTMLElement; | |
while (source && source.tagName !== 'TR' && source !== evt.currentTarget) { | |
source = source.parentElement; | |
} | |
if (!source || source.tagName !== 'TR') { | |
return null; | |
} | |
const { clientX, clientY } = evt; | |
start(source as HTMLTableRowElement, [clientX, clientY]); | |
}); | |
table.addEventListener('mousemove', mousemoveListener = (evt: MouseEvent) => { | |
if (!context) { | |
return; | |
} | |
const { clientX: currentX, clientY: currentY } = evt; | |
const [startX, startY] = context.startPosition; | |
const dx = currentX - startX; | |
const dy = currentY - startY; | |
// 拖动超过 10 像素再开启拖动 | |
if (!context.activated && Math.abs(dy) > 10 && Math.abs(dy / dx) > 2) { | |
activate(); | |
} | |
// 拖动已开启,执行 move 逻辑 | |
if (context.activated) { | |
evt.preventDefault(); | |
move(dy); | |
} | |
}); | |
window.addEventListener('mouseup', mouseupListener = () => { | |
if (context) { | |
drop(); | |
} | |
}); | |
}; | |
// 清理事件 | |
const destroy = () => { | |
table.removeEventListener('mousedown', mousedownListener); | |
table.removeEventListener('mousemove', mousemoveListener); | |
window.removeEventListener('mouseup', mouseupListener); | |
}; | |
return setup(), { destroy }; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment