Fix text bug on firefox, translate cloning for grouped shapes

This commit is contained in:
Steve Ruiz 2021-09-02 21:13:54 +01:00
parent a3ddfca0be
commit fe9ff2dc2d
13 changed files with 291 additions and 269 deletions

View file

@ -39,11 +39,13 @@ export interface RendererProps<T extends TLShape, M extends Record<string, unkno
*/
hideHandles?: boolean
/**
* When true, the renderer will not show indicators for selected or hovered objects,
* When true, the renderer will not show indicators for selected or
* hovered objects,
*/
hideIndicators?: boolean
/**
* When true, the renderer will ignore all inputs that were not made by a stylus or pen-type device.
* When true, the renderer will ignore all inputs that were not made
* by a stylus or pen-type device.
*/
isPenMode?: boolean
/**
@ -53,24 +55,10 @@ export interface RendererProps<T extends TLShape, M extends Record<string, unkno
}
/**
The Renderer component is the main component of the library. It accepts the current `page`, the `shapeUtils` needed to interpret and render the shapes and bindings on the `page`, and the current `pageState`.
* It also (optionally) accepts several settings and visibility flags,
* a `theme` to use, and callbacks to respond to various user interactions.
*
* ### Example
*
*```tsx
* <Renderer
* shapeUtils={shapeUtils}
* page={page}
* pageState={pageState}
* />
*```
*/
/**
* The Renderer component is the main component of the library. It accepts the current `page`, the `shapeUtils` needed to interpret and render the shapes and bindings on the `page`, and the current `pageState`.
* The Renderer component is the main component of the library. It
* accepts the current `page`, the `shapeUtils` needed to interpret
* and render the shapes and bindings on the `page`, and the current
* `pageState`.
* @param props
* @returns
*/
@ -86,6 +74,7 @@ export function Renderer<T extends TLShape, M extends Record<string, unknown>>({
...rest
}: RendererProps<T, M>): JSX.Element {
useTLTheme(theme)
const rScreenBounds = React.useRef<TLBounds>(null)
const rPageState = React.useRef<TLPageState>(pageState)

View file

@ -23,6 +23,15 @@ export function EditingTextShape({
const ref = React.useRef<HTMLElement>(null)
React.useEffect(() => {
// Firefox fix?
setTimeout(() => {
if (document.activeElement !== ref.current) {
ref.current?.focus()
}
}, 0)
}, [shape.id])
return utils.render(shape, {
ref,
isEditing,

View file

@ -5,13 +5,17 @@ import type { Data } from '~types'
const isDebugModeSelector = (s: Data) => s.settings.isDebugMode
const isDarkModeSelector = (s: Data) => s.settings.isDarkMode
const isZoomSnapSelector = (s: Data) => s.settings.isZoomSnap
export function Preferences() {
const { tlstate, useSelector } = useTLDrawContext()
const isDebugMode = useSelector(isDebugModeSelector)
const isDarkMode = useSelector(isDarkModeSelector)
const isZoomSnap = useSelector(isZoomSnapSelector)
const toggleDebugMode = React.useCallback(() => {
tlstate.toggleDebugMode()
}, [tlstate])
@ -20,11 +24,18 @@ export function Preferences() {
tlstate.toggleDarkMode()
}, [tlstate])
const toggleZoomSnap = React.useCallback(() => {
tlstate.toggleZoomSnap()
}, [tlstate])
return (
<DropdownMenuSubMenu label="Preferences">
<DropdownMenuCheckboxItem checked={isDarkMode} onCheckedChange={toggleDarkMode}>
<span>Dark Mode</span>
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem checked={isZoomSnap} onCheckedChange={toggleZoomSnap}>
<span>Zoom Snap</span>
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem checked={isDebugMode} onCheckedChange={toggleDebugMode}>
<span>Debug Mode</span>
</DropdownMenuCheckboxItem>

View file

@ -4,7 +4,7 @@ import { Renderer } from '@tldraw/core'
import styled from '~styles'
import type { Data, TLDrawDocument } from '~types'
import { TLDrawState } from '~state'
import { useKeyboardShortcuts, TLDrawContext, useCustomFonts } from '~hooks'
import { TLDrawContext, useCustomFonts, useKeyboardShortcuts, useTLDrawContext } from '~hooks'
import { tldrawShapeUtils } from '~shape'
import { ContextMenu } from '~components/context-menu'
import { StylePanel } from '~components/style-panel'
@ -61,19 +61,45 @@ export function TLDraw({ id, document, currentPageId, onMount, onChange: _onChan
return { tlstate, useSelector: tlstate.useStore }
})
useKeyboardShortcuts(tlstate)
React.useEffect(() => {
if (!document) return
tlstate.loadDocument(document, _onChange)
}, [document, tlstate])
React.useEffect(() => {
if (!currentPageId) return
tlstate.changePage(currentPageId)
}, [currentPageId, tlstate])
React.useEffect(() => {
onMount?.(tlstate)
}, [])
return (
<TLDrawContext.Provider value={context}>
<IdProvider>
<InnerTldraw />
</IdProvider>
</TLDrawContext.Provider>
)
}
function InnerTldraw() {
useCustomFonts()
const page = context.useSelector(pageSelector)
const { tlstate, useSelector } = useTLDrawContext()
const pageState = context.useSelector(pageStateSelector)
useKeyboardShortcuts()
const isDarkMode = context.useSelector(isDarkModeSelector)
const page = useSelector(pageSelector)
const isSelecting = context.useSelector(isInSelectSelector)
const pageState = useSelector(pageStateSelector)
const isSelectedHandlesShape = context.useSelector(isSelectedShapeWithHandlesSelector)
const isDarkMode = useSelector(isDarkModeSelector)
const isSelecting = useSelector(isInSelectSelector)
const isSelectedHandlesShape = useSelector(isSelectedShapeWithHandlesSelector)
const isInSession = tlstate.session !== undefined
@ -89,20 +115,7 @@ export function TLDraw({ id, document, currentPageId, onMount, onChange: _onChan
// Custom rendering meta, with dark mode for shapes
const meta = React.useMemo(() => ({ isDarkMode }), [isDarkMode])
React.useEffect(() => {
if (!document) return
tlstate.loadDocument(document, _onChange)
}, [document, tlstate])
React.useEffect(() => {
if (!currentPageId) return
tlstate.changePage(currentPageId)
}, [currentPageId, tlstate])
React.useEffect(() => {
onMount?.(tlstate)
}, [])
// Custom theme, based on darkmode
const theme = React.useMemo(() => {
if (isDarkMode) {
return {
@ -119,80 +132,76 @@ export function TLDraw({ id, document, currentPageId, onMount, onChange: _onChan
}, [isDarkMode])
return (
<TLDrawContext.Provider value={context}>
<IdProvider>
<Layout>
<ContextMenu>
<Renderer
page={page}
pageState={pageState}
shapeUtils={tldrawShapeUtils}
theme={theme}
meta={meta}
hideBounds={hideBounds}
hideHandles={hideHandles}
hideIndicators={hideIndicators}
onPinchStart={tlstate.onPinchStart}
onPinchEnd={tlstate.onPinchEnd}
onPinch={tlstate.onPinch}
onPan={tlstate.onPan}
onZoom={tlstate.onZoom}
onPointerDown={tlstate.onPointerDown}
onPointerMove={tlstate.onPointerMove}
onPointerUp={tlstate.onPointerUp}
onPointCanvas={tlstate.onPointCanvas}
onDoubleClickCanvas={tlstate.onDoubleClickCanvas}
onRightPointCanvas={tlstate.onRightPointCanvas}
onDragCanvas={tlstate.onDragCanvas}
onReleaseCanvas={tlstate.onReleaseCanvas}
onPointShape={tlstate.onPointShape}
onDoubleClickShape={tlstate.onDoubleClickShape}
onRightPointShape={tlstate.onRightPointShape}
onDragShape={tlstate.onDragShape}
onHoverShape={tlstate.onHoverShape}
onUnhoverShape={tlstate.onUnhoverShape}
onReleaseShape={tlstate.onReleaseShape}
onPointBounds={tlstate.onPointBounds}
onDoubleClickBounds={tlstate.onDoubleClickBounds}
onRightPointBounds={tlstate.onRightPointBounds}
onDragBounds={tlstate.onDragBounds}
onHoverBounds={tlstate.onHoverBounds}
onUnhoverBounds={tlstate.onUnhoverBounds}
onReleaseBounds={tlstate.onReleaseBounds}
onPointBoundsHandle={tlstate.onPointBoundsHandle}
onDoubleClickBoundsHandle={tlstate.onDoubleClickBoundsHandle}
onRightPointBoundsHandle={tlstate.onRightPointBoundsHandle}
onDragBoundsHandle={tlstate.onDragBoundsHandle}
onHoverBoundsHandle={tlstate.onHoverBoundsHandle}
onUnhoverBoundsHandle={tlstate.onUnhoverBoundsHandle}
onReleaseBoundsHandle={tlstate.onReleaseBoundsHandle}
onPointHandle={tlstate.onPointHandle}
onDoubleClickHandle={tlstate.onDoubleClickHandle}
onRightPointHandle={tlstate.onRightPointHandle}
onDragHandle={tlstate.onDragHandle}
onHoverHandle={tlstate.onHoverHandle}
onUnhoverHandle={tlstate.onUnhoverHandle}
onReleaseHandle={tlstate.onReleaseHandle}
onChange={tlstate.onChange}
onError={tlstate.onError}
onBlurEditingShape={tlstate.onBlurEditingShape}
onTextBlur={tlstate.onTextBlur}
onTextChange={tlstate.onTextChange}
onTextKeyDown={tlstate.onTextKeyDown}
onTextFocus={tlstate.onTextFocus}
onTextKeyUp={tlstate.onTextKeyUp}
/>
</ContextMenu>
<MenuButtons>
<Menu />
<PagePanel />
</MenuButtons>
<Spacer />
<StylePanel />
<ToolsPanel />
</Layout>
</IdProvider>
</TLDrawContext.Provider>
<Layout>
<ContextMenu>
<Renderer
page={page}
pageState={pageState}
shapeUtils={tldrawShapeUtils}
theme={theme}
meta={meta}
hideBounds={hideBounds}
hideHandles={hideHandles}
hideIndicators={hideIndicators}
onPinchStart={tlstate.onPinchStart}
onPinchEnd={tlstate.onPinchEnd}
onPinch={tlstate.onPinch}
onPan={tlstate.onPan}
onZoom={tlstate.onZoom}
onPointerDown={tlstate.onPointerDown}
onPointerMove={tlstate.onPointerMove}
onPointerUp={tlstate.onPointerUp}
onPointCanvas={tlstate.onPointCanvas}
onDoubleClickCanvas={tlstate.onDoubleClickCanvas}
onRightPointCanvas={tlstate.onRightPointCanvas}
onDragCanvas={tlstate.onDragCanvas}
onReleaseCanvas={tlstate.onReleaseCanvas}
onPointShape={tlstate.onPointShape}
onDoubleClickShape={tlstate.onDoubleClickShape}
onRightPointShape={tlstate.onRightPointShape}
onDragShape={tlstate.onDragShape}
onHoverShape={tlstate.onHoverShape}
onUnhoverShape={tlstate.onUnhoverShape}
onReleaseShape={tlstate.onReleaseShape}
onPointBounds={tlstate.onPointBounds}
onDoubleClickBounds={tlstate.onDoubleClickBounds}
onRightPointBounds={tlstate.onRightPointBounds}
onDragBounds={tlstate.onDragBounds}
onHoverBounds={tlstate.onHoverBounds}
onUnhoverBounds={tlstate.onUnhoverBounds}
onReleaseBounds={tlstate.onReleaseBounds}
onPointBoundsHandle={tlstate.onPointBoundsHandle}
onDoubleClickBoundsHandle={tlstate.onDoubleClickBoundsHandle}
onRightPointBoundsHandle={tlstate.onRightPointBoundsHandle}
onDragBoundsHandle={tlstate.onDragBoundsHandle}
onHoverBoundsHandle={tlstate.onHoverBoundsHandle}
onUnhoverBoundsHandle={tlstate.onUnhoverBoundsHandle}
onReleaseBoundsHandle={tlstate.onReleaseBoundsHandle}
onPointHandle={tlstate.onPointHandle}
onDoubleClickHandle={tlstate.onDoubleClickHandle}
onRightPointHandle={tlstate.onRightPointHandle}
onDragHandle={tlstate.onDragHandle}
onHoverHandle={tlstate.onHoverHandle}
onUnhoverHandle={tlstate.onUnhoverHandle}
onReleaseHandle={tlstate.onReleaseHandle}
onChange={tlstate.onChange}
onError={tlstate.onError}
onBlurEditingShape={tlstate.onBlurEditingShape}
onTextBlur={tlstate.onTextBlur}
onTextChange={tlstate.onTextChange}
onTextKeyDown={tlstate.onTextKeyDown}
onTextFocus={tlstate.onTextFocus}
onTextKeyUp={tlstate.onTextKeyUp}
/>
</ContextMenu>
<MenuButtons>
<Menu />
<PagePanel />
</MenuButtons>
<Spacer />
<StylePanel />
<ToolsPanel />
</Layout>
)
}

View file

@ -2,9 +2,11 @@ import * as React from 'react'
import { inputs } from '@tldraw/core'
import { useHotkeys } from 'react-hotkeys-hook'
import { TLDrawShapeType } from '~types'
import type { TLDrawState } from '~state'
import { useTLDrawContext } from '~hooks'
export function useKeyboardShortcuts() {
const { tlstate } = useTLDrawContext()
export function useKeyboardShortcuts(tlstate: TLDrawState) {
React.useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
const info = inputs.keydown(e)
@ -18,6 +20,7 @@ export function useKeyboardShortcuts(tlstate: TLDrawState) {
window.addEventListener('keydown', handleKeyDown)
window.addEventListener('keyup', handleKeyUp)
return () => {
window.removeEventListener('keydown', handleKeyDown)
window.removeEventListener('keyup', handleKeyUp)

View file

@ -107,7 +107,6 @@ export class Text extends TLDrawShapeUtil<TextShape> {
const { id, text, style } = shape
const styles = getShapeStyle(style, meta.isDarkMode)
const font = getFontStyle(shape.style)
const bounds = this.getBounds(shape)
function handleChange(e: React.ChangeEvent<HTMLTextAreaElement>) {
@ -150,8 +149,10 @@ export class Text extends TLDrawShapeUtil<TextShape> {
}
function handleFocus(e: React.FocusEvent<HTMLTextAreaElement>) {
e.currentTarget.select()
onTextFocus?.(id)
if (document.activeElement === e.currentTarget) {
e.currentTarget.select()
onTextFocus?.(id)
}
}
function handlePointerDown() {
@ -161,6 +162,7 @@ export class Text extends TLDrawShapeUtil<TextShape> {
}
const fontSize = getFontSize(shape.style.size) * (shape.style.scale || 1)
const lineHeight = fontSize * 1.3
if (!isEditing) {

View file

@ -118,6 +118,7 @@ export class ArrowSession implements Session {
// From all bindable shapes on the page...
for (const id of this.bindableShapeIds) {
if (id === initialShape.id) continue
if (id === shape.parentId) continue
if (id === oppositeBinding?.toId) continue
target = TLDR.getShape(data, id, data.appState.currentPageId)
@ -345,6 +346,7 @@ function findBinding(
// From all bindable shapes on the page...
for (const id of bindableShapeIds) {
if (id === shape.id) continue
if (id === shape.parentId) continue
if (id === oppositeBinding?.toId) continue
const target = TLDR.getShape(data, id, data.appState.currentPageId)

View file

@ -1,6 +1,6 @@
import { TLDrawState } from '~state'
import { mockDocument } from '~test'
import { ArrowShape, TLDrawShapeType, TLDrawStatus } from '~types'
import { TLDrawShapeType, TLDrawStatus } from '~types'
describe('Translate session', () => {
const tlstate = new TLDrawState()
@ -111,19 +111,28 @@ describe('Translate session', () => {
})
it('destroys clones when last update is not cloning', () => {
tlstate.resetDocument().loadDocument(mockDocument)
expect(Object.keys(tlstate.getPage().shapes).length).toBe(3)
tlstate
.loadDocument(mockDocument)
.select('rect1', 'rect2')
.startTranslateSession([10, 10])
.updateTranslateSession([20, 20], false, true)
.updateTranslateSession([30, 30])
.completeSession()
expect(Object.keys(tlstate.getPage().shapes).length).toBe(5)
tlstate.updateTranslateSession([30, 30], false, false)
expect(Object.keys(tlstate.getPage().shapes).length).toBe(3)
tlstate.completeSession()
// Original position + delta
expect(tlstate.getShape('rect1').point).toStrictEqual([30, 30])
expect(tlstate.getShape('rect2').point).toStrictEqual([130, 130])
expect(Object.keys(tlstate.getPage().shapes).length).toBe(3)
expect(Object.keys(tlstate.page.shapes)).toStrictEqual(['rect1', 'rect2', 'rect3'])
})
it('destroys bindings from the translating shape', () => {
@ -175,5 +184,25 @@ describe('Translate session', () => {
// it.todo('clones a shape with a parent shape')
// it.todo('clones a shape with children')
describe('when translating a shape with children', () => {
it('translates the shapes children', () => {
tlstate
.loadDocument(mockDocument)
.select('rect1', 'rect2')
.group()
.startTranslateSession([10, 10])
.updateTranslateSession([20, 20], false, false)
.completeSession()
})
it('clones the shapes and children', () => {
tlstate
.loadDocument(mockDocument)
.select('rect1', 'rect2')
.group()
.startTranslateSession([10, 10])
.updateTranslateSession([20, 20], false, true)
.completeSession()
})
})
})

View file

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { TLPageState, Utils, Vec } from '@tldraw/core'
import {
TLDrawShape,
@ -46,8 +47,10 @@ export class TranslateSession implements Session {
}
update = (data: Data, point: number[], isAligned = false, isCloning = false) => {
const { clones, initialShapes, bindingsToDelete } = this.snapshot
const { selectedIds, initialParentChildren, clones, initialShapes, bindingsToDelete } =
this.snapshot
const { currentPageId } = data.appState
const nextBindings: Patch<Record<string, TLDrawBinding>> = {}
const nextShapes: Patch<Record<string, TLDrawShape>> = {}
const nextPageState: Patch<TLPageState> = {}
@ -74,26 +77,40 @@ export class TranslateSession implements Session {
this.isCloning = true
// Put back any bindings we deleted
bindingsToDelete.forEach((binding) => (nextBindings[binding.id] = binding))
// Move original shapes back to start
initialShapes.forEach((shape) => (nextShapes[shape.id] = { point: shape.point }))
// Add the clones to the page
clones.forEach((clone) => {
nextShapes[clone.id] = { ...clone, point: Vec.round(Vec.add(clone.point, delta)) }
// Add clones to non-selected parents
if (
clone.parentId !== data.appState.currentPageId &&
!selectedIds.includes(clone.parentId)
) {
const children =
nextShapes[clone.parentId]?.children || initialParentChildren[clone.parentId]
nextShapes[clone.parentId] = {
...nextShapes[clone.parentId],
children: [...children, clone.id],
}
}
})
nextPageState.selectedIds = clones.map((shape) => shape.id)
// Add the cloned bindings
for (const binding of this.snapshot.clonedBindings) {
nextBindings[binding.id] = binding
}
// Set the selected ids to the clones
nextPageState.selectedIds = clones.map((clone) => clone.id)
}
// Either way, move the clones
clones.forEach((shape) => {
const current = (nextShapes[shape.id] ||
TLDR.getShape(data, shape.id, data.appState.currentPageId)) as TLDrawShape
@ -117,7 +134,15 @@ export class TranslateSession implements Session {
bindingsToDelete.forEach((binding) => (nextBindings[binding.id] = undefined))
// Delete the clones
clones.forEach((clone) => (nextShapes[clone.id] = undefined))
clones.forEach((clone) => {
nextShapes[clone.id] = undefined
if (clone.parentId !== currentPageId) {
nextShapes[clone.parentId] = {
...nextShapes[clone.parentId],
children: initialParentChildren[clone.parentId],
}
}
})
// Move the original shapes back to the cursor position
initialShapes.forEach((shape) => {
@ -211,9 +236,9 @@ export class TranslateSession implements Session {
const afterBindings: Patch<Record<string, TLDrawBinding>> = {}
const afterShapes: Patch<Record<string, TLDrawShape>> = {}
clones.forEach((shape) => {
beforeShapes[shape.id] = undefined
afterShapes[shape.id] = this.isCloning ? TLDR.getShape(data, shape.id, pageId) : undefined
clones.forEach((clone) => {
beforeShapes[clone.id] = undefined
afterShapes[clone.id] = this.isCloning ? TLDR.getShape(data, clone.id, pageId) : undefined
})
initialShapes.forEach((shape) => {
@ -296,21 +321,27 @@ export class TranslateSession implements Session {
export function getTranslateSnapshot(data: Data) {
const { currentPageId } = data.appState
const selectedIds = TLDR.getSelectedIds(data, currentPageId)
const page = TLDR.getPage(data, currentPageId)
const selectedShapes = TLDR.getSelectedShapeSnapshot(data, currentPageId)
const selectedShapes = selectedIds.flatMap((id) => TLDR.getShape(data, id, currentPageId))
const hasUnlockedShapes = selectedShapes.length > 0
const page = TLDR.getPage(data, currentPageId)
const shapesToMove: TLDrawShape[] = selectedShapes
.filter((shape) => !selectedIds.includes(shape.parentId))
.flatMap((shape) => {
return shape.children
? [shape, ...shape.children!.map((childId) => TLDR.getShape(data, childId, currentPageId))]
: [shape]
})
const initialParents = Array.from(new Set(selectedShapes.map((s) => s.parentId)).values())
const initialParentChildren: Record<string, string[]> = {}
Array.from(new Set(shapesToMove.map((s) => s.parentId)).values())
.filter((id) => id !== page.id)
.map((id) => {
.forEach((id) => {
const shape = TLDR.getShape(data, id, currentPageId)
return {
id,
children: shape.children,
}
initialParentChildren[id] = shape.children!
})
const cloneMap: Record<string, string> = {}
@ -318,35 +349,31 @@ export function getTranslateSnapshot(data: Data) {
const clonedBindings: TLDrawBinding[] = []
// Create clones of selected shapes
const clones = selectedShapes.flatMap((shape) => {
const clones: TLDrawShape[] = []
shapesToMove.forEach((shape) => {
const newId = Utils.uniqueId()
cloneMap[shape.id] = newId
const clone: TLDrawShape = {
clones.push({
...Utils.deepClone(shape),
id: newId,
parentId: shape.parentId,
childIndex: TLDR.getChildIndexAbove(data, shape.id, currentPageId),
})
})
clones.forEach((clone) => {
if (clone.children !== undefined) {
clone.children = clone.children.map((childId) => cloneMap[childId])
}
})
if (!shape.children) return clone
// If the shape has children, also create clones for the children
return [
clone,
...shape.children.map((childId) => {
const child = TLDR.getShape(data, childId, currentPageId)
const newChildId = Utils.uniqueId()
cloneMap[shape.id] = newChildId
return {
...Utils.deepClone(child),
id: newChildId,
parentId: shape.parentId,
childIndex: TLDR.getChildIndexAbove(data, child.id, currentPageId),
}
}),
]
clones.forEach((clone) => {
if (selectedIds.includes(clone.parentId)) {
clone.parentId = cloneMap[clone.parentId]
}
})
// Potentially confusing name here: these are the ids of the
@ -388,12 +415,18 @@ export function getTranslateSnapshot(data: Data) {
}
})
clones.forEach((clone) => {
if (page.shapes[clone.id]) {
throw Error("uh oh, we didn't clone correctly")
}
})
return {
selectedIds,
bindingsToDelete,
hasUnlockedShapes,
initialParents,
initialShapes: selectedShapes.map(({ id, point, parentId }) => ({
initialParentChildren,
initialShapes: shapesToMove.map(({ id, point, parentId }) => ({
id,
point,
parentId,

View file

@ -14,6 +14,7 @@ import {
Vec,
brushUpdater,
TLPointerInfo,
inputs,
} from '@tldraw/core'
import {
FlipType,
@ -67,6 +68,7 @@ const initialData: Data = {
settings: {
isPenMode: false,
isDarkMode: false,
isZoomSnap: true,
isDebugMode: process.env.NODE_ENV === 'development',
isReadonlyMode: false,
nudgeDistanceLarge: 10,
@ -92,7 +94,7 @@ const initialData: Data = {
}
export class TLDrawState extends StateManager<Data> {
_onChange?: (tlstate: TLDrawState, data: Data, reason: string) => void
private _onChange?: (tlstate: TLDrawState, data: Data, reason: string) => void
selectHistory: SelectHistory = {
stack: [[]],
@ -112,7 +114,15 @@ export class TLDrawState extends StateManager<Data> {
isCreating = false
constructor(id = Utils.uniqueId()) {
super(initialData, id, 1)
super(initialData, id, 2, (prev, next, prevVersion) => {
const state = { ...prev }
if (prevVersion === 1)
state.settings = {
...state.settings,
isZoomSnap: next.settings.isZoomSnap,
}
return state
})
this.session = undefined
this.pointedId = undefined
@ -182,7 +192,7 @@ export class TLDrawState extends StateManager<Data> {
// (unless the group is being deleted too)
if (parentId && parentId !== pageId) {
const group = page.shapes[parentId]
if (group) {
if (group !== undefined) {
groupsToUpdate.add(page.shapes[parentId] as GroupShape)
}
}
@ -352,6 +362,19 @@ export class TLDrawState extends StateManager<Data> {
return this
}
/**
* Toggle zoom snap.
* @returns this
*/
toggleZoomSnap = () => {
this.patchState(
{ settings: { isZoomSnap: !this.state.settings.isZoomSnap } },
`settings:toggled_zoom_snap`
)
this.persist()
return this
}
/**
* Toggle debug mode.
* @returns this
@ -419,6 +442,7 @@ export class TLDrawState extends StateManager<Data> {
resetDocument = (): this => {
this.loadDocument(defaultDocument)
this.persist()
return this
}
@ -906,9 +930,8 @@ export class TLDrawState extends StateManager<Data> {
* @param next The new zoom level.
* @returns this
*/
zoomTo = (next: number): this => {
zoomTo = (next: number, center = [window.innerWidth / 2, window.innerHeight / 2]): this => {
const { zoom, point } = this.pageState.camera
const center = [window.innerWidth / 2, window.innerHeight / 2]
const p0 = Vec.sub(Vec.div(center, zoom), point)
const p1 = Vec.sub(Vec.div(center, next), point)
return this.setCamera(Vec.round(Vec.add(point, Vec.sub(p1, p0))), next, `zoomed_camera`)
@ -1534,7 +1557,6 @@ export class TLDrawState extends StateManager<Data> {
* @returns this
*/
style = (style: Partial<ShapeStyles>, ids = this.selectedIds): this => {
if (ids.length === 0) return this
return this.setState(Commands.style(this.state, ids, style))
}
@ -2006,7 +2028,7 @@ export class TLDrawState extends StateManager<Data> {
}
case TLDrawStatus.Translating: {
if (key === 'Shift' || key === 'Alt') {
this.updateTransformSession(this.getPagePoint(info.point), info.shiftKey, info.altKey)
this.updateTranslateSession(this.getPagePoint(info.point), info.shiftKey, info.altKey)
}
break
}
@ -2035,6 +2057,11 @@ export class TLDrawState extends StateManager<Data> {
}
onPinchEnd: TLPinchEventHandler = () => {
if (this.state.settings.isZoomSnap) {
const i = Math.round((this.pageState.camera.zoom * 100) / 25)
const nextZoom = TLDR.getCameraZoom(i * 0.25)
this.zoomTo(nextZoom, inputs.pointer?.point)
}
this.setStatus(this.appState.status.previous)
}

View file

@ -11,7 +11,7 @@ export const Wrapper: React.FC = ({ children }) => {
return { tlstate, useSelector: tlstate.useStore }
})
useKeyboardShortcuts(tlstate)
useKeyboardShortcuts()
React.useEffect(() => {
if (!document) return

View file

@ -23,6 +23,7 @@ export interface TLDrawSettings {
isDebugMode: boolean
isPenMode: boolean
isReadonlyMode: boolean
isZoomSnap: boolean
nudgeDistanceSmall: number
nudgeDistanceLarge: number
}

View file

@ -1,99 +1,6 @@
import * as React from 'react'
import { ColorStyle, DashStyle, SizeStyle, TLDrawShapeType, TLDrawState } from '@tldraw/tldraw'
import { TLDraw, TLDrawDocument, TLDrawPatch } from '@tldraw/tldraw'
import { usePersistence } from '../hooks/usePersistence'
const initialDoc: TLDrawDocument = {
id: 'doc',
pages: {
page1: {
id: 'page1',
shapes: {
rect1: {
id: 'rect1',
parentId: 'page1',
name: 'Rectangle',
childIndex: 1,
type: TLDrawShapeType.Rectangle,
point: [32, 32],
size: [100, 100],
style: {
dash: DashStyle.Draw,
size: SizeStyle.Medium,
color: ColorStyle.Blue,
},
},
ellipse1: {
id: 'ellipse1',
parentId: 'page1',
name: 'Ellipse',
childIndex: 2,
type: TLDrawShapeType.Ellipse,
point: [132, 132],
radius: [50, 50],
style: {
dash: DashStyle.Draw,
size: SizeStyle.Medium,
color: ColorStyle.Cyan,
},
},
draw1: {
id: 'draw1',
parentId: 'page1',
name: 'Draw',
childIndex: 3,
type: TLDrawShapeType.Draw,
point: [232, 232],
points: [
[50, 0],
[100, 100],
[0, 100],
[50, 0],
[100, 100],
[0, 100],
[50, 0],
[56, 5],
],
style: {
dash: DashStyle.Draw,
size: SizeStyle.Medium,
color: ColorStyle.Green,
},
},
},
bindings: {},
},
},
pageStates: {
page1: {
id: 'page1',
selectedIds: [],
currentParentId: 'page1',
camera: {
point: [0, 0],
zoom: 1,
},
},
},
}
import { TLDraw } from '@tldraw/tldraw'
export default function Editor(): JSX.Element {
const { value, setValue, status } = usePersistence('doc', initialDoc)
const handleChange = React.useCallback(
(tlstate: TLDrawState, patch: TLDrawPatch, reason: string) => {
if (reason.startsWith('session')) {
return
}
setValue(tlstate.document)
},
[setValue]
)
if (status === 'loading' || value === null) {
return <div />
}
return <TLDraw document={value} onChange={handleChange} />
return <TLDraw id="tldraw" />
}