kopia lustrzana https://github.com/Tldraw/Tldraw
225 wiersze
7.3 KiB
TypeScript
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')
|
|
}
|
|
}
|