
This PR improves the ergonomics of `ShapeUtil` classes. ### Cached methods First, I've remove the cached methods (such as `bounds`) from the `ShapeUtil` class and lifted this to the `Editor` class. Previously, calling `ShapeUtil.getBounds` would return the un-cached bounds of a shape, while calling `ShapeUtil.bounds` would return the cached bounds of a shape. We also had `Editor.getBounds`, which would call `ShapeUtil.bounds`. It was confusing. The cached methods like `outline` were also marked with "please don't override", which suggested the architecture was just wrong. The only weirdness from this is that utils sometimes reach out to the editor for cached versions of data rather than calling their own cached methods. It's still an easier story to tell than what we had before. ### More defaults We now have three and only three `abstract` methods for a `ShapeUtil`: - `getDefaultProps` (renamed from `defaultProps`) - `getBounds`, - `component` - `indicator` Previously, we also had `getCenter` as an abstract method, though this was usually just the middle of the bounds anyway. ### Editing bounds This PR removes the concept of editingBounds. The viewport will no longer animate to editing shapes. ### Active area manager This PR also removes the active area manager, which was not being used in the way we expected it to be. ### Dpr manager This PR removes the dpr manager and uses a hook instead to update it from React. This is one less runtime browser dependency in the app, one less thing to document. ### Moving things around This PR also continues to try to organize related methods and properties in the editor. ### Change Type - [x] `major` — Breaking change ### Release Notes - [editor] renames `defaultProps` to `getDefaultProps` - [editor] removes `outline`, `outlineSegments`, `handles`, `bounds` - [editor] renames `renderBackground` to `backgroundComponent`
285 lines
7.1 KiB
TypeScript
285 lines
7.1 KiB
TypeScript
import { useEffect } from 'react'
|
|
import { useValue } from 'signia-react'
|
|
import { TLKeyboardEventInfo, TLPointerEventInfo } from '../editor/types/event-types'
|
|
import { preventDefault } from '../utils/dom'
|
|
import { useContainer } from './useContainer'
|
|
import { useEditor } from './useEditor'
|
|
|
|
export function useDocumentEvents() {
|
|
const editor = useEditor()
|
|
const container = useContainer()
|
|
|
|
const isAppFocused = useValue('isFocused', () => editor.isFocused, [editor])
|
|
|
|
useEffect(() => {
|
|
if (typeof matchMedia !== undefined) return
|
|
|
|
function updateDevicePixelRatio() {
|
|
editor.setDevicePixelRatio(window.devicePixelRatio)
|
|
}
|
|
|
|
const MM = matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`)
|
|
|
|
MM.addEventListener('change', updateDevicePixelRatio)
|
|
return () => {
|
|
MM.removeEventListener('change', updateDevicePixelRatio)
|
|
}
|
|
}, [editor])
|
|
|
|
useEffect(() => {
|
|
if (!isAppFocused) return
|
|
|
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
if (
|
|
e.altKey &&
|
|
(editor.isIn('zoom') || !editor.root.path.value.endsWith('.idle')) &&
|
|
!isFocusingInput()
|
|
) {
|
|
// On windows the alt key opens the menu bar.
|
|
// We want to prevent that if the user is doing something else,
|
|
// e.g. resizing a shape
|
|
preventDefault(e)
|
|
}
|
|
|
|
if ((e as any).isKilled) return
|
|
;(e as any).isKilled = true
|
|
|
|
switch (e.key) {
|
|
case '=': {
|
|
if (e.metaKey || e.ctrlKey) {
|
|
preventDefault(e)
|
|
return
|
|
}
|
|
break
|
|
}
|
|
case '-': {
|
|
if (e.metaKey || e.ctrlKey) {
|
|
preventDefault(e)
|
|
return
|
|
}
|
|
break
|
|
}
|
|
case '0': {
|
|
if (e.metaKey || e.ctrlKey) {
|
|
preventDefault(e)
|
|
return
|
|
}
|
|
break
|
|
}
|
|
case 'Tab': {
|
|
if (isFocusingInput() || editor.isMenuOpen) {
|
|
return
|
|
}
|
|
break
|
|
}
|
|
case ',': {
|
|
if (!isFocusingInput()) {
|
|
preventDefault(e)
|
|
if (!editor.inputs.keys.has('Comma')) {
|
|
const { x, y, z } = editor.inputs.currentScreenPoint
|
|
const {
|
|
pageState: { hoveredId },
|
|
} = editor
|
|
editor.inputs.keys.add('Comma')
|
|
|
|
const info: TLPointerEventInfo = {
|
|
type: 'pointer',
|
|
name: 'pointer_down',
|
|
point: { x, y, z },
|
|
shiftKey: e.shiftKey,
|
|
altKey: e.altKey,
|
|
ctrlKey: e.metaKey || e.ctrlKey,
|
|
pointerId: 0,
|
|
button: 0,
|
|
isPen: editor.isPenMode,
|
|
...(hoveredId
|
|
? {
|
|
target: 'shape',
|
|
shape: editor.getShapeById(hoveredId)!,
|
|
}
|
|
: {
|
|
target: 'canvas',
|
|
}),
|
|
}
|
|
|
|
editor.dispatch(info)
|
|
return
|
|
}
|
|
}
|
|
break
|
|
}
|
|
case 'Escape': {
|
|
if (!editor.inputs.keys.has('Escape')) {
|
|
editor.inputs.keys.add('Escape')
|
|
|
|
editor.cancel()
|
|
// Pressing escape will focus the document.body,
|
|
// which will cause the app to lose focus, which
|
|
// will break additional shortcuts. We need to
|
|
// refocus the container in order to keep these
|
|
// shortcuts working.
|
|
container.focus()
|
|
}
|
|
return
|
|
}
|
|
default: {
|
|
if (isFocusingInput() || editor.isMenuOpen) {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
const info: TLKeyboardEventInfo = {
|
|
type: 'keyboard',
|
|
name: editor.inputs.keys.has(e.code) ? 'key_repeat' : 'key_down',
|
|
key: e.key,
|
|
code: e.code,
|
|
shiftKey: e.shiftKey,
|
|
altKey: e.altKey,
|
|
ctrlKey: e.metaKey || e.ctrlKey,
|
|
}
|
|
|
|
editor.dispatch(info)
|
|
}
|
|
|
|
const handleKeyUp = (e: KeyboardEvent) => {
|
|
if ((e as any).isKilled) return
|
|
;(e as any).isKilled = true
|
|
|
|
if (isFocusingInput() || editor.isMenuOpen) {
|
|
return
|
|
}
|
|
|
|
// Use the , key to send pointer events
|
|
if (e.key === ',') {
|
|
if (document.activeElement?.ELEMENT_NODE) preventDefault(e)
|
|
if (editor.inputs.keys.has(e.code)) {
|
|
const { x, y, z } = editor.inputs.currentScreenPoint
|
|
const {
|
|
pageState: { hoveredId },
|
|
} = editor
|
|
|
|
editor.inputs.keys.delete(e.code)
|
|
|
|
const info: TLPointerEventInfo = {
|
|
type: 'pointer',
|
|
name: 'pointer_up',
|
|
point: { x, y, z },
|
|
shiftKey: e.shiftKey,
|
|
altKey: e.altKey,
|
|
ctrlKey: e.metaKey || e.ctrlKey,
|
|
pointerId: 0,
|
|
button: 0,
|
|
isPen: editor.isPenMode,
|
|
...(hoveredId
|
|
? {
|
|
target: 'shape',
|
|
shape: editor.getShapeById(hoveredId)!,
|
|
}
|
|
: {
|
|
target: 'canvas',
|
|
}),
|
|
}
|
|
editor.dispatch(info)
|
|
return
|
|
}
|
|
}
|
|
|
|
const info: TLKeyboardEventInfo = {
|
|
type: 'keyboard',
|
|
name: 'key_up',
|
|
key: e.key,
|
|
code: e.code,
|
|
shiftKey: e.shiftKey,
|
|
altKey: e.altKey,
|
|
ctrlKey: e.metaKey || e.ctrlKey,
|
|
}
|
|
|
|
editor.dispatch(info)
|
|
}
|
|
|
|
function handleTouchStart(e: TouchEvent) {
|
|
if (container.contains(e.target as Node)) {
|
|
// Center point of the touch area
|
|
const touchXPosition = e.touches[0].pageX
|
|
// Size of the touch area
|
|
const touchXRadius = e.touches[0].radiusX || 0
|
|
|
|
// We set a threshold (10px) on both sizes of the screen,
|
|
// if the touch area overlaps with the screen edges
|
|
// it's likely to trigger the navigation. We prevent the
|
|
// touchstart event in that case.
|
|
if (
|
|
touchXPosition - touchXRadius < 10 ||
|
|
touchXPosition + touchXRadius > editor.viewportScreenBounds.width - 10
|
|
) {
|
|
if ((e.target as HTMLElement)?.tagName === 'BUTTON') {
|
|
// Force a click before bailing
|
|
;(e.target as HTMLButtonElement)?.click()
|
|
}
|
|
|
|
preventDefault(e)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Prevent wheel events that occur inside of the container
|
|
const handleWheel = (e: WheelEvent) => {
|
|
if (container.contains(e.target as Node) && (e.ctrlKey || e.metaKey)) {
|
|
preventDefault(e)
|
|
}
|
|
}
|
|
|
|
function handleBlur() {
|
|
editor.complete()
|
|
}
|
|
|
|
function handleFocus() {
|
|
editor.updateViewportScreenBounds()
|
|
}
|
|
|
|
container.addEventListener('touchstart', handleTouchStart, { passive: false })
|
|
|
|
document.addEventListener('wheel', handleWheel, { passive: false })
|
|
document.addEventListener('gesturestart', preventDefault)
|
|
document.addEventListener('gesturechange', preventDefault)
|
|
document.addEventListener('gestureend', preventDefault)
|
|
|
|
document.addEventListener('keydown', handleKeyDown)
|
|
document.addEventListener('keyup', handleKeyUp)
|
|
|
|
window.addEventListener('blur', handleBlur)
|
|
window.addEventListener('focus', handleFocus)
|
|
|
|
return () => {
|
|
container.removeEventListener('touchstart', handleTouchStart)
|
|
|
|
document.removeEventListener('wheel', handleWheel)
|
|
document.removeEventListener('gesturestart', preventDefault)
|
|
document.removeEventListener('gesturechange', preventDefault)
|
|
document.removeEventListener('gestureend', preventDefault)
|
|
|
|
document.removeEventListener('keydown', handleKeyDown)
|
|
document.removeEventListener('keyup', handleKeyUp)
|
|
|
|
window.removeEventListener('blur', handleBlur)
|
|
window.removeEventListener('focus', handleFocus)
|
|
}
|
|
}, [editor, container, isAppFocused])
|
|
}
|
|
|
|
const INPUTS = ['input', 'select', 'button', 'textarea']
|
|
|
|
function isFocusingInput() {
|
|
const { activeElement } = document
|
|
|
|
if (
|
|
activeElement &&
|
|
(activeElement.getAttribute('contenteditable') ||
|
|
INPUTS.indexOf(activeElement.tagName.toLowerCase()) > -1)
|
|
) {
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|