UNPKG

tldraw

Version:

A tiny little drawing editor.

145 lines (115 loc) 4.41 kB
import { Editor, TLShape, TLShapeId, Vec, bind, compact } from '@tldraw/editor' import { getOccludedChildren } 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: number | 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) { clearTimeout(this.droppingNodeTimer) this.setDragTimer(movingShapes, FAST_POINTER_LAG_DURATION, cb) } } private setDragTimer(movingShapes: TLShape[], duration: number, cb: () => void) { this.droppingNodeTimer = this.editor.timers.setTimeout(() => { this.editor.run(() => { 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, TLShapeId[]>() for (const shape of movingShapes) { const ancestor = this.editor.findShapeAncestor(shape, (v) => v.type !== 'group') if (!ancestor) continue if (!shapesGroupedByAncestor.has(ancestor.id)) { shapesGroupedByAncestor.set(ancestor.id, []) } shapesGroupedByAncestor.get(ancestor.id)!.push(shape.id) } // Only hint an ancestor if some shapes will drop into it on pointer up const hintingShapes = [] for (const [ancestorId, shapeIds] of shapesGroupedByAncestor) { const ancestor = this.editor.getShape(ancestorId) if (!ancestor) continue // If all of the ancestor's children would be occluded, then don't hint it // 1. get the number of fully occluded children // 2. if that number is less than the number of moving shapes, hint the ancestor if (getOccludedChildren(this.editor, ancestor).length < shapeIds.length) { 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) { clearTimeout(this.droppingNodeTimer) } this.droppingNodeTimer = null this.editor.setHintingShapes([]) this.first = true } @bind dispose() { this.clear() } }