Tldraw/packages/tldraw/src/lib/tools/SelectTool/childStates/PointingShape.ts

225 wiersze
7.3 KiB
TypeScript

import {
Group2d,
HIT_TEST_MARGIN,
StateNode,
TLArrowShape,
TLEventHandlers,
TLGeoShape,
TLPointerEventInfo,
TLShape,
} from '@tldraw/editor'
export class PointingShape extends StateNode {
static override id = 'pointing_shape'
hitShape = {} as TLShape
hitShapeForPointerUp = {} as TLShape
didSelectOnEnter = false
override onEnter = (info: TLPointerEventInfo & { target: 'shape' }) => {
const selectedShapeIds = this.editor.getSelectedShapeIds()
const selectionBounds = this.editor.getSelectionRotatedPageBounds()
const focusedGroupId = this.editor.getFocusedGroupId()
const {
inputs: { currentPagePoint, shiftKey, altKey },
} = this.editor
this.hitShape = info.shape
const outermostSelectingShape = this.editor.getOutermostSelectableShape(info.shape)
if (
// If the shape has an onClick handler
this.editor.getShapeUtil(info.shape).onClick ||
// ...or if the shape is the focused layer (e.g. group)
outermostSelectingShape.id === focusedGroupId ||
// ...or if the shape is within the selection
selectedShapeIds.includes(outermostSelectingShape.id) ||
this.editor.isAncestorSelected(outermostSelectingShape.id) ||
// ...or if the current point is NOT within the selection bounds
(selectedShapeIds.length > 1 && selectionBounds?.containsPoint(currentPagePoint))
) {
// We won't select the shape on enter, though we might select it on pointer up!
this.didSelectOnEnter = false
this.hitShapeForPointerUp = outermostSelectingShape
return
}
this.didSelectOnEnter = true
if (shiftKey && !altKey) {
this.editor.cancelDoubleClick()
if (!selectedShapeIds.includes(outermostSelectingShape.id)) {
this.editor.mark('shift selecting shape')
this.editor.setSelectedShapes([...selectedShapeIds, outermostSelectingShape.id])
}
} else {
this.editor.mark('selecting shape')
this.editor.setSelectedShapes([outermostSelectingShape.id])
}
}
override onPointerUp: TLEventHandlers['onPointerUp'] = (info) => {
const selectedShapeIds = this.editor.getSelectedShapeIds()
const focusedGroupId = this.editor.getFocusedGroupId()
const zoomLevel = this.editor.getZoomLevel()
const {
inputs: { currentPagePoint, shiftKey },
} = this.editor
const hitShape =
this.editor.getShapeAtPoint(currentPagePoint, {
margin: HIT_TEST_MARGIN / zoomLevel,
hitInside: true,
renderingOnly: true,
}) ?? this.hitShape
const selectingShape = hitShape
? this.editor.getOutermostSelectableShape(hitShape)
: this.hitShapeForPointerUp
if (selectingShape) {
// If the selecting shape has a click handler, call it instead of selecting the shape
const util = this.editor.getShapeUtil(selectingShape)
if (util.onClick) {
const change = util.onClick?.(selectingShape)
if (change) {
this.editor.mark('shape on click')
this.editor.updateShapes([change])
this.parent.transition('idle', info)
return
}
}
if (selectingShape.id === focusedGroupId) {
if (selectedShapeIds.length > 0) {
this.editor.mark('clearing shape ids')
this.editor.setSelectedShapes([])
} else {
this.editor.popFocusedGroupId()
}
this.parent.transition('idle', info)
return
}
}
if (!this.didSelectOnEnter) {
// if the shape has an ancestor which is a focusable layer and it is not focused but it is selected
// then we should focus the layer and select the shape
const outermostSelectableShape = this.editor.getOutermostSelectableShape(
hitShape,
// if a group is selected, we want to stop before reaching that group
// so we can drill down into the group
(parent) => !selectedShapeIds.includes(parent.id)
)
// If the outermost shape is selected, then either select or deselect the SELECTING shape
if (selectedShapeIds.includes(outermostSelectableShape.id)) {
// same shape, so deselect it if shift is pressed, otherwise deselect all others
if (shiftKey) {
this.editor.mark('deselecting on pointer up')
this.editor.deselect(selectingShape)
} else {
if (selectedShapeIds.includes(selectingShape.id)) {
// todo
// if the shape is editable and we're inside of an editable part of that shape, e.g. the label of a geo shape,
// then we would want to begin editing the shape. At the moment we're relying on the shape label's onPointerUp
// handler to do this logic, and prevent the regular pointer up event, so we won't be here in that case.
// ! tldraw hack
// if the shape is a geo shape, and we're inside of the label, then we want to begin editing the label
if (
selectedShapeIds.length === 1 &&
(this.editor.isShapeOfType<TLGeoShape>(selectingShape, 'geo') ||
this.editor.isShapeOfType<TLArrowShape>(selectingShape, 'arrow'))
) {
const geometry = this.editor.getShapeGeometry(selectingShape)
const labelGeometry = (geometry as Group2d).children[1]
if (labelGeometry) {
const pointInShapeSpace = this.editor.getPointInShapeSpace(
selectingShape,
currentPagePoint
)
if (
labelGeometry.bounds.containsPoint(pointInShapeSpace, 0) &&
labelGeometry.hitTestPoint(pointInShapeSpace)
) {
this.editor.mark('editing on pointer up')
this.editor.select(selectingShape.id)
const util = this.editor.getShapeUtil(selectingShape)
if (this.editor.getInstanceState().isReadonly) {
if (!util.canEditInReadOnly(selectingShape)) {
return
}
}
this.editor.setEditingShape(selectingShape.id)
this.editor.setCurrentTool('select.editing_shape')
return
}
}
}
// We just want to select the single shape from the selection
this.editor.mark('selecting on pointer up')
this.editor.select(selectingShape.id)
} else {
this.editor.mark('selecting on pointer up')
this.editor.select(selectingShape)
}
}
} else if (shiftKey) {
// Different shape, so we are drilling down into a group with shift key held.
// Deselect any ancestors and add the target shape to the selection
const ancestors = this.editor.getShapeAncestors(outermostSelectableShape)
this.editor.mark('shift deselecting on pointer up')
this.editor.setSelectedShapes([
...this.editor.getSelectedShapeIds().filter((id) => !ancestors.find((a) => a.id === id)),
outermostSelectableShape.id,
])
} else {
this.editor.mark('selecting on pointer up')
// different shape and we are drilling down, but no shift held so just select it straight up
this.editor.setSelectedShapes([outermostSelectableShape.id])
}
}
this.parent.transition('idle', info)
}
override onPointerMove: TLEventHandlers['onPointerMove'] = (info) => {
if (this.editor.inputs.isDragging) {
this.startTranslating(info)
}
}
override onLongPress: TLEventHandlers['onLongPress'] = (info) => {
this.startTranslating(info)
}
private startTranslating(info: TLPointerEventInfo) {
if (this.editor.getInstanceState().isReadonly) return
this.parent.transition('translating', info)
}
override onCancel: TLEventHandlers['onCancel'] = () => {
this.cancel()
}
override onComplete: TLEventHandlers['onComplete'] = () => {
this.cancel()
}
override onInterrupt = () => {
this.cancel()
}
private cancel() {
this.parent.transition('idle')
}
}