kopia lustrzana https://github.com/Tldraw/Tldraw
244 wiersze
8.2 KiB
TypeScript
244 wiersze
8.2 KiB
TypeScript
import {
|
|
HIT_TEST_MARGIN,
|
|
StateNode,
|
|
TLEventHandlers,
|
|
TLPointerEventInfo,
|
|
TLShape,
|
|
} from '@tldraw/editor'
|
|
import { getTextLabels } from '../../../utils/shapes/shapes'
|
|
|
|
export class PointingShape extends StateNode {
|
|
static override id = 'pointing_shape'
|
|
|
|
hitShape = {} as TLShape
|
|
hitShapeForPointerUp = {} as TLShape
|
|
isDoubleClick = false
|
|
|
|
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
|
|
this.isDoubleClick = false
|
|
const outermostSelectingShape = this.editor.getOutermostSelectableShape(info.shape)
|
|
const selectedAncestor = this.editor.findShapeAncestor(outermostSelectingShape, (parent) =>
|
|
selectedShapeIds.includes(parent.id)
|
|
)
|
|
|
|
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) ||
|
|
// ...or if an ancestor of the shape is selected (except note shapes)...
|
|
// todo: Consider adding a flag for this hardcoded behaviour
|
|
(selectedAncestor && selectedAncestor.type !== 'note') ||
|
|
// ...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.
|
|
|
|
// if the shape has a text label, and we're inside of the label, then we want to begin editing the label.
|
|
if (selectedShapeIds.length === 1) {
|
|
const geometry = this.editor.getShapeUtil(selectingShape).getGeometry(selectingShape)
|
|
const textLabels = getTextLabels(geometry)
|
|
const textLabel = textLabels.length === 1 ? textLabels[0] : undefined
|
|
// N.B. we're only interested if there is exactly one text label. We don't handle the
|
|
// case if there's potentially more than one text label at the moment.
|
|
if (textLabel) {
|
|
const pointInShapeSpace = this.editor.getPointInShapeSpace(
|
|
selectingShape,
|
|
currentPagePoint
|
|
)
|
|
|
|
if (
|
|
textLabel.bounds.containsPoint(pointInShapeSpace, 0) &&
|
|
textLabel.hitTestPoint(pointInShapeSpace)
|
|
) {
|
|
this.editor.batch(() => {
|
|
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')
|
|
|
|
if (this.isDoubleClick) {
|
|
// XXX this is a hack to select the text in the textarea when we hit enter.
|
|
// Open to other ideas! I don't see how else to currently do this in the codebase.
|
|
;(
|
|
document.getElementById(
|
|
`text-input-${selectingShape.id}`
|
|
) as HTMLTextAreaElement
|
|
).select()
|
|
}
|
|
})
|
|
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 onDoubleClick: TLEventHandlers['onDoubleClick'] = () => {
|
|
this.isDoubleClick = true
|
|
}
|
|
|
|
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')
|
|
}
|
|
}
|