tldraw
Version:
A tiny little drawing editor.
207 lines (172 loc) • 6.28 kB
text/typescript
import {
Editor,
IndexKey,
TLGroupShape,
TLParentId,
TLShape,
TLShapeId,
Vec,
bind,
compact,
isShapeId,
} from '@tldraw/editor'
const SLOW_POINTER_LAG_DURATION = 320
const FAST_POINTER_LAG_DURATION = 60
/** @public */
export class DragAndDropManager {
constructor(public editor: Editor) {
editor.disposables.add(this.dispose)
}
shapesToActuallyMove: TLShape[] = []
draggedOverShapeIds = new Set<TLShapeId>()
initialGroupIds = new Map<TLShapeId, TLShapeId>()
initialParentIds = new Map<TLShapeId, TLParentId>()
initialIndices = new Map<TLShapeId, IndexKey>()
initialDraggingOverShape?: TLShape
prevDraggingOverShape?: TLShape
prevPagePoint = new Vec()
intervalTimerId = -1
startDraggingShapes(movingShapes: TLShape[], point: Vec, cb: () => void) {
const { editor } = this
// Only start dragging if we're not already dragging
if (this.intervalTimerId !== -1) return
const shapesToActuallyMove = new Set(movingShapes)
const movingGroups = new Set<TLGroupShape>()
for (const shape of shapesToActuallyMove) {
const parent = editor.getShapeParent(shape)
if (parent && editor.isShapeOfType(parent, 'group')) {
if (!movingGroups.has(parent)) {
movingGroups.add(parent)
}
}
}
// If all of a group's children are moving, then move the group instead
for (const movingGroup of movingGroups) {
const children = compact(
editor.getSortedChildIdsForParent(movingGroup).map((id) => editor.getShape(id))
)
shapesToActuallyMove.add(movingGroup)
for (const child of children) {
shapesToActuallyMove.delete(child)
}
}
this.initialParentIds.clear()
for (const shape of shapesToActuallyMove) {
const parent = editor.getShapeParent(shape)
if (parent) {
this.initialParentIds.set(shape.id, parent.id)
}
this.initialIndices.set(shape.id, shape.index)
const group = editor.findShapeAncestor(shape, (s) => editor.isShapeOfType(s, 'group'))
if (group) {
this.initialGroupIds.set(shape.id, group.id)
}
}
const allShapes = editor.getCurrentPageShapesSorted()
this.shapesToActuallyMove = Array.from(shapesToActuallyMove)
.filter((s) => !s.isLocked)
.sort((a, b) => allShapes.indexOf(a) - allShapes.indexOf(b))
this.initialDraggingOverShape = editor.getDraggingOverShape(point, this.shapesToActuallyMove)
this.prevDraggingOverShape = this.initialDraggingOverShape
// run once on first frame
this.updateDraggingShapes(point, cb)
// then once on an interval, skipping frames if moving quickly
let skip2of3FramesWhileMovingFast = 0
this.intervalTimerId = this.editor.timers.setInterval(
() => {
skip2of3FramesWhileMovingFast++
if (
skip2of3FramesWhileMovingFast % 3 &&
this.editor.inputs.getPointerVelocity().len() > 0.5
) {
return
}
this.updateDraggingShapes(editor.inputs.getCurrentPagePoint(), cb)
},
movingShapes.length > 10 ? SLOW_POINTER_LAG_DURATION : FAST_POINTER_LAG_DURATION
)
}
dropShapes(shapes: TLShape[]) {
const { editor } = this
const currentPagePoint = editor.inputs.getCurrentPagePoint()
this.updateDraggingShapes(currentPagePoint)
const draggingOverShape = editor.getDraggingOverShape(currentPagePoint, shapes)
if (draggingOverShape) {
const util = editor.getShapeUtil(draggingOverShape)
util.onDropShapesOver?.(draggingOverShape, shapes, {
initialDraggingOverShapeId: this.initialDraggingOverShape?.id ?? null,
initialParentIds: this.initialParentIds,
initialIndices: this.initialIndices,
})
}
this.dispose()
}
clear() {
clearInterval(this.intervalTimerId)
this.intervalTimerId = -1
this.initialParentIds.clear()
this.initialIndices.clear()
this.shapesToActuallyMove = []
this.initialDraggingOverShape = undefined
this.prevDraggingOverShape = undefined
this.editor.setHintingShapes([])
}
dispose() {
this.clear()
}
private updateDraggingShapes(point: Vec, cb?: () => void): void {
const { editor } = this
// get fresh moving shapes
const draggingShapes = compact(this.shapesToActuallyMove.map((s) => editor.getShape(s)))
if (!draggingShapes.length) return
// This is the shape under the pointer that can handle at least one of the dragging shapes
const nextDraggingOverShape = editor.getDraggingOverShape(point, this.shapesToActuallyMove)
const currentPagePoint = editor.inputs.getCurrentPagePoint()
const cursorDidMove = !this.prevPagePoint.equals(currentPagePoint)
this.prevPagePoint.setTo(currentPagePoint)
editor.run(() => {
if (this.prevDraggingOverShape?.id === nextDraggingOverShape?.id) {
if (
cursorDidMove &&
nextDraggingOverShape &&
isShapeId(nextDraggingOverShape.id) &&
!editor.inputs.getPreviousPagePoint().equals(currentPagePoint)
) {
// If the cursor moved, call onDragShapesOver for the previous dragging over shape
const util = editor.getShapeUtil(nextDraggingOverShape)
util.onDragShapesOver?.(nextDraggingOverShape, draggingShapes, {
initialDraggingOverShapeId: this.initialDraggingOverShape?.id ?? null,
initialParentIds: this.initialParentIds,
initialIndices: this.initialIndices,
})
}
return
}
if (this.prevDraggingOverShape) {
const util = editor.getShapeUtil(this.prevDraggingOverShape)
util.onDragShapesOut?.(this.editor.getShape(this.prevDraggingOverShape)!, draggingShapes, {
nextDraggingOverShapeId: nextDraggingOverShape?.id ?? null,
initialDraggingOverShapeId: this.initialDraggingOverShape?.id ?? null,
initialParentIds: this.initialParentIds,
initialIndices: this.initialIndices,
})
}
if (nextDraggingOverShape) {
const util = editor.getShapeUtil(nextDraggingOverShape)
util.onDragShapesIn?.(nextDraggingOverShape, draggingShapes, {
initialDraggingOverShapeId: this.initialDraggingOverShape?.id ?? null,
prevDraggingOverShapeId: this.prevDraggingOverShape?.id ?? null,
initialParentIds: this.initialParentIds,
initialIndices: this.initialIndices,
})
editor.setHintingShapes([nextDraggingOverShape.id])
} else if (this.prevDraggingOverShape) {
editor.setHintingShapes([])
}
// This is the reparenting logic
cb?.()
})
this.prevDraggingOverShape = nextDraggingOverShape
}
}