kopia lustrzana https://github.com/Tldraw/Tldraw
140 wiersze
4.1 KiB
TypeScript
140 wiersze
4.1 KiB
TypeScript
import { Editor, TLShape, TLShapeId, Vec, compact } from '@tldraw/editor'
|
|
import { isShapeOccluded } from './selectHelpers'
|
|
|
|
const INITIAL_POINTER_LAG_DURATION = 20
|
|
const FAST_POINTER_LAG_DURATION = 100
|
|
|
|
/** @public */
|
|
export class DragAndDropManager {
|
|
constructor(public editor: Editor) {
|
|
editor.disposables.add(this.dispose)
|
|
}
|
|
|
|
prevDroppingShapeId: TLShapeId | null = null
|
|
|
|
droppingNodeTimer: ReturnType<typeof setTimeout> | null = null
|
|
|
|
first = true
|
|
|
|
updateDroppingNode(movingShapes: TLShape[], cb: () => void) {
|
|
if (this.first) {
|
|
this.editor.setHintingShapes(
|
|
movingShapes
|
|
.map((s) => this.editor.findShapeAncestor(s, (v) => v.type !== 'group'))
|
|
.filter((s) => s) as TLShape[]
|
|
)
|
|
|
|
this.prevDroppingShapeId =
|
|
this.editor.getDroppingOverShape(this.editor.inputs.originPagePoint, movingShapes)?.id ??
|
|
null
|
|
this.first = false
|
|
}
|
|
|
|
if (this.droppingNodeTimer === null) {
|
|
this.setDragTimer(movingShapes, INITIAL_POINTER_LAG_DURATION, cb)
|
|
} else if (this.editor.inputs.pointerVelocity.len() > 0.5) {
|
|
clearInterval(this.droppingNodeTimer)
|
|
this.setDragTimer(movingShapes, FAST_POINTER_LAG_DURATION, cb)
|
|
}
|
|
}
|
|
|
|
private setDragTimer(movingShapes: TLShape[], duration: number, cb: () => void) {
|
|
this.droppingNodeTimer = setTimeout(() => {
|
|
this.editor.batch(() => {
|
|
this.handleDrag(this.editor.inputs.currentPagePoint, movingShapes, cb)
|
|
})
|
|
this.droppingNodeTimer = null
|
|
}, duration)
|
|
}
|
|
|
|
private handleDrag(point: Vec, movingShapes: TLShape[], cb?: () => void) {
|
|
movingShapes = compact(movingShapes.map((shape) => this.editor.getShape(shape.id)))
|
|
|
|
const nextDroppingShapeId = this.editor.getDroppingOverShape(point, movingShapes)?.id ?? null
|
|
|
|
// is the next dropping shape id different than the last one?
|
|
if (nextDroppingShapeId === this.prevDroppingShapeId) {
|
|
this.hintParents(movingShapes)
|
|
return
|
|
}
|
|
|
|
// the old previous one
|
|
const { prevDroppingShapeId } = this
|
|
|
|
const prevDroppingShape = prevDroppingShapeId && this.editor.getShape(prevDroppingShapeId)
|
|
const nextDroppingShape = nextDroppingShapeId && this.editor.getShape(nextDroppingShapeId)
|
|
|
|
// Even if we don't have a next dropping shape id (i.e. if we're dropping
|
|
// onto the page) set the prev to the current, to avoid repeat calls to
|
|
// the previous parent's onDragShapesOut
|
|
|
|
if (prevDroppingShape) {
|
|
this.editor.getShapeUtil(prevDroppingShape).onDragShapesOut?.(prevDroppingShape, movingShapes)
|
|
}
|
|
|
|
if (nextDroppingShape) {
|
|
this.editor
|
|
.getShapeUtil(nextDroppingShape)
|
|
.onDragShapesOver?.(nextDroppingShape, movingShapes)
|
|
}
|
|
|
|
this.hintParents(movingShapes)
|
|
cb?.()
|
|
|
|
// next -> curr
|
|
this.prevDroppingShapeId = nextDroppingShapeId
|
|
}
|
|
|
|
hintParents(movingShapes: TLShape[]) {
|
|
// Group moving shapes by their ancestor
|
|
const shapesGroupedByAncestor = new Map<TLShapeId, TLShape[]>()
|
|
for (const shape of movingShapes) {
|
|
const ancestor = this.editor.findShapeAncestor(shape, (v) => v.type !== 'group')
|
|
if (!ancestor) continue
|
|
const shapes = shapesGroupedByAncestor.get(ancestor.id) ?? []
|
|
shapes.push(shape)
|
|
shapesGroupedByAncestor.set(ancestor.id, shapes)
|
|
}
|
|
|
|
// Only hint an ancestor if some shapes will drop into it on pointer up
|
|
const hintingShapes = []
|
|
for (const [ancestorId, shapes] of shapesGroupedByAncestor) {
|
|
const ancestor = this.editor.getShape(ancestorId)
|
|
if (!ancestor) continue
|
|
if (shapes.some((shape) => !isShapeOccluded(this.editor, ancestor, shape.id))) {
|
|
hintingShapes.push(ancestor.id)
|
|
}
|
|
}
|
|
|
|
this.editor.setHintingShapes(hintingShapes)
|
|
}
|
|
|
|
dropShapes(shapes: TLShape[]) {
|
|
const { prevDroppingShapeId } = this
|
|
|
|
this.handleDrag(this.editor.inputs.currentPagePoint, shapes)
|
|
|
|
if (prevDroppingShapeId) {
|
|
const shape = this.editor.getShape(prevDroppingShapeId)
|
|
if (!shape) return
|
|
this.editor.getShapeUtil(shape).onDropShapesOver?.(shape, shapes)
|
|
}
|
|
}
|
|
|
|
clear() {
|
|
this.prevDroppingShapeId = null
|
|
|
|
if (this.droppingNodeTimer !== null) {
|
|
clearInterval(this.droppingNodeTimer)
|
|
}
|
|
|
|
this.droppingNodeTimer = null
|
|
this.editor.setHintingShapes([])
|
|
this.first = true
|
|
}
|
|
|
|
dispose = () => {
|
|
this.clear()
|
|
}
|
|
}
|