Fix text bug on firefox, translate cloning for grouped shapes
This commit is contained in:
parent
a3ddfca0be
commit
fe9ff2dc2d
13 changed files with 291 additions and 269 deletions
|
@ -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)
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ export const Wrapper: React.FC = ({ children }) => {
|
|||
return { tlstate, useSelector: tlstate.useStore }
|
||||
})
|
||||
|
||||
useKeyboardShortcuts(tlstate)
|
||||
useKeyboardShortcuts()
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!document) return
|
||||
|
|
|
@ -23,6 +23,7 @@ export interface TLDrawSettings {
|
|||
isDebugMode: boolean
|
||||
isPenMode: boolean
|
||||
isReadonlyMode: boolean
|
||||
isZoomSnap: boolean
|
||||
nudgeDistanceSmall: number
|
||||
nudgeDistanceLarge: number
|
||||
}
|
||||
|
|
|
@ -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" />
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue