Tldraw/packages/editor/src/lib/editor/managers/ClickManager.ts

222 wiersze
4.8 KiB
TypeScript
Czysty Zwykły widok Historia

import {
COARSE_DRAG_DISTANCE,
DOUBLE_CLICK_DURATION,
DRAG_DISTANCE,
MULTI_CLICK_DURATION,
} from '../../constants'
import { Vec } from '../../primitives/Vec'
tldraw zero - package shuffle (#1710) This PR moves code between our packages so that: - @tldraw/editor is a “core” library with the engine and canvas but no shapes, tools, or other things - @tldraw/tldraw contains everything particular to the experience we’ve built for tldraw At first look, this might seem like a step away from customization and configuration, however I believe it greatly increases the configuration potential of the @tldraw/editor while also providing a more accurate reflection of what configuration options actually exist for @tldraw/tldraw. ## Library changes @tldraw/editor re-exports its dependencies and @tldraw/tldraw re-exports @tldraw/editor. - users of @tldraw/editor WITHOUT @tldraw/tldraw should almost always only import things from @tldraw/editor. - users of @tldraw/tldraw should almost always only import things from @tldraw/tldraw. - @tldraw/polyfills is merged into @tldraw/editor - @tldraw/indices is merged into @tldraw/editor - @tldraw/primitives is merged mostly into @tldraw/editor, partially into @tldraw/tldraw - @tldraw/file-format is merged into @tldraw/tldraw - @tldraw/ui is merged into @tldraw/tldraw Many (many) utils and other code is moved from the editor to tldraw. For example, embeds now are entirely an feature of @tldraw/tldraw. The only big chunk of code left in core is related to arrow handling. ## API Changes The editor can now be used without tldraw's assets. We load them in @tldraw/tldraw instead, so feel free to use whatever fonts or images or whatever that you like with the editor. All tools and shapes (except for the `Group` shape) are moved to @tldraw/tldraw. This includes the `select` tool. You should use the editor with at least one tool, however, so you now also need to send in an `initialState` prop to the Editor / <TldrawEditor> component indicating which state the editor should begin in. The `components` prop now also accepts `SelectionForeground`. The complex selection component that we use for tldraw is moved to @tldraw/tldraw. The default component is quite basic but can easily be replaced via the `components` prop. We pass down our tldraw-flavored SelectionFg via `components`. Likewise with the `Scribble` component: the `DefaultScribble` no longer uses our freehand tech and is a simple path instead. We pass down the tldraw-flavored scribble via `components`. The `ExternalContentManager` (`Editor.externalContentManager`) is removed and replaced with a mapping of types to handlers. - Register new content handlers with `Editor.registerExternalContentHandler`. - Register new asset creation handlers (for files and URLs) with `Editor.registerExternalAssetHandler` ### Change Type - [x] `major` — Breaking change ### Test Plan - [x] Unit Tests - [x] End to end tests ### Release Notes - [@tldraw/editor] lots, wip - [@tldraw/ui] gone, merged to tldraw/tldraw - [@tldraw/polyfills] gone, merged to tldraw/editor - [@tldraw/primitives] gone, merged to tldraw/editor / tldraw/tldraw - [@tldraw/indices] gone, merged to tldraw/editor - [@tldraw/file-format] gone, merged to tldraw/tldraw --------- Co-authored-by: alex <alex@dytry.ch>
2023-07-17 21:22:34 +00:00
import { uniqueId } from '../../utils/uniqueId'
import type { Editor } from '../Editor'
2023-04-25 11:01:25 +00:00
import { TLClickEventInfo, TLPointerEventInfo } from '../types/event-types'
type TLClickState =
| 'idle'
| 'pendingDouble'
| 'pendingTriple'
| 'pendingQuadruple'
| 'pendingOverflow'
| 'overflow'
const MAX_CLICK_DISTANCE = 40
export class ClickManager {
constructor(public editor: Editor) {}
2023-04-25 11:01:25 +00:00
private _clickId = ''
private _clickTimeout?: any
private _clickScreenPoint?: Vec
2023-04-25 11:01:25 +00:00
private _previousScreenPoint?: Vec
2023-04-25 11:01:25 +00:00
private _getClickTimeout = (state: TLClickState, id = uniqueId()) => {
this._clickId = id
clearTimeout(this._clickTimeout)
this._clickTimeout = setTimeout(
() => {
if (this._clickState === state && this._clickId === id) {
switch (this._clickState) {
case 'pendingTriple': {
this.editor.dispatch({
2023-04-25 11:01:25 +00:00
...this.lastPointerInfo,
type: 'click',
name: 'double_click',
phase: 'settle',
})
break
}
case 'pendingQuadruple': {
this.editor.dispatch({
2023-04-25 11:01:25 +00:00
...this.lastPointerInfo,
type: 'click',
name: 'triple_click',
phase: 'settle',
})
break
}
case 'pendingOverflow': {
this.editor.dispatch({
2023-04-25 11:01:25 +00:00
...this.lastPointerInfo,
type: 'click',
name: 'quadruple_click',
phase: 'settle',
})
break
}
default: {
// noop
}
}
this._clickState = 'idle'
}
},
state === 'idle' || state === 'pendingDouble' ? DOUBLE_CLICK_DURATION : MULTI_CLICK_DURATION
)
}
/**
* The current click state.
*
* @internal
*/
private _clickState?: TLClickState = 'idle'
/**
* The current click state.
*
* @public
*/
// eslint-disable-next-line no-restricted-syntax
2023-04-25 11:01:25 +00:00
get clickState() {
return this._clickState
}
lastPointerInfo = {} as TLPointerEventInfo
2024-04-18 10:37:54 +00:00
handlePointerEvent = (info: TLPointerEventInfo): TLPointerEventInfo | TLClickEventInfo => {
switch (info.name) {
case 'pointer_down': {
if (!this._clickState) return info
this._clickScreenPoint = Vec.From(info.point)
2023-04-25 11:01:25 +00:00
2024-04-18 10:37:54 +00:00
if (
this._previousScreenPoint &&
2024-04-18 14:28:48 +00:00
Vec.Dist2(this._previousScreenPoint, this._clickScreenPoint) > MAX_CLICK_DISTANCE ** 2
2024-04-18 10:37:54 +00:00
) {
this._clickState = 'idle'
}
2023-04-25 11:01:25 +00:00
2024-04-18 10:37:54 +00:00
this._previousScreenPoint = this._clickScreenPoint
2023-04-25 11:01:25 +00:00
2024-04-18 10:37:54 +00:00
this.lastPointerInfo = info
2023-04-25 11:01:25 +00:00
2024-04-18 10:37:54 +00:00
switch (this._clickState) {
case 'pendingDouble': {
this._clickState = 'pendingTriple'
this._clickTimeout = this._getClickTimeout(this._clickState)
return {
...info,
type: 'click',
name: 'double_click',
phase: 'down',
}
}
case 'pendingTriple': {
this._clickState = 'pendingQuadruple'
this._clickTimeout = this._getClickTimeout(this._clickState)
return {
...info,
type: 'click',
name: 'triple_click',
phase: 'down',
}
}
case 'pendingQuadruple': {
this._clickState = 'pendingOverflow'
this._clickTimeout = this._getClickTimeout(this._clickState)
return {
...info,
type: 'click',
name: 'quadruple_click',
phase: 'down',
}
}
2024-04-18 14:28:48 +00:00
case 'idle': {
this._clickState = 'pendingDouble'
break
}
2024-04-18 10:37:54 +00:00
case 'pendingOverflow': {
this._clickState = 'overflow'
2024-04-18 14:28:48 +00:00
break
2024-04-18 10:37:54 +00:00
}
default: {
// overflow
}
2023-04-25 11:01:25 +00:00
}
2024-04-18 14:28:48 +00:00
this._clickTimeout = this._getClickTimeout(this._clickState)
return info
2023-04-25 11:01:25 +00:00
}
2024-04-18 10:37:54 +00:00
case 'pointer_up': {
if (!this._clickState) return info
this._clickScreenPoint = Vec.From(info.point)
switch (this._clickState) {
case 'pendingTriple': {
return {
...this.lastPointerInfo,
type: 'click',
name: 'double_click',
phase: 'up',
}
}
case 'pendingQuadruple': {
return {
...this.lastPointerInfo,
type: 'click',
name: 'triple_click',
phase: 'up',
}
}
case 'pendingOverflow': {
return {
...this.lastPointerInfo,
type: 'click',
name: 'quadruple_click',
phase: 'up',
}
}
default: {
// idle, pendingDouble, overflow
}
2023-04-25 11:01:25 +00:00
}
2024-04-18 14:28:48 +00:00
return info
2023-04-25 11:01:25 +00:00
}
2024-04-18 10:37:54 +00:00
case 'pointer_move': {
if (
this._clickState !== 'idle' &&
this._clickScreenPoint &&
Vec.Dist2(this._clickScreenPoint, this.editor.inputs.currentScreenPoint) >
(this.editor.getInstanceState().isCoarsePointer ? COARSE_DRAG_DISTANCE : DRAG_DISTANCE)
) {
this.cancelDoubleClickTimeout()
2023-04-25 11:01:25 +00:00
}
return info
}
}
2024-04-18 10:37:54 +00:00
return info
2023-04-25 11:01:25 +00:00
}
/**
* Cancel the double click timeout.
*
* @internal
*/
cancelDoubleClickTimeout = () => {
this._clickTimeout = clearTimeout(this._clickTimeout)
this._clickState = 'idle'
}
}