Fix rendering bug, tweak API

This commit is contained in:
Steve Ruiz 2021-10-27 17:16:07 +01:00
parent a906a3bd95
commit c04e4134d2
15 changed files with 664 additions and 653 deletions

View file

@ -28,7 +28,7 @@ export const RenderedShape = React.memo(
// consider using layout effect to update bounds cache if the ref is filled // consider using layout effect to update bounds cache if the ref is filled
return ( return (
<utils._Component <utils.Component
ref={ref} ref={ref}
shape={shape} shape={shape}
isEditing={isEditing} isEditing={isEditing}

View file

@ -2,12 +2,12 @@
import * as React from 'react' import * as React from 'react'
/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-unused-vars */
import type { TLShape, TLBounds } from '+types' import type { TLShape, TLBounds, TLComponentProps } from '+types'
import { TLShapeUtil } from './TLShapeUtil' import { TLShapeUtil } from './TLShapeUtil'
import { render } from '@testing-library/react' import { render } from '@testing-library/react'
import { SVGContainer } from '+components' import { SVGContainer } from '+components'
import Utils from '+utils' import Utils from '+utils'
import type { TLComponent, TLIndicator } from '+shape-utils' import type { TLIndicator } from '+shape-utils'
export interface BoxShape extends TLShape { export interface BoxShape extends TLShape {
type: 'box' type: 'box'
@ -32,18 +32,20 @@ export const boxShape: BoxShape = {
export class BoxUtil extends TLShapeUtil<BoxShape, SVGSVGElement, Meta> { export class BoxUtil extends TLShapeUtil<BoxShape, SVGSVGElement, Meta> {
age = 100 age = 100
Component: TLComponent<BoxShape, SVGSVGElement, Meta> = ({ shape, events, meta }, ref) => { Component = React.forwardRef<SVGSVGElement, TLComponentProps<BoxShape, SVGSVGElement>>(
type T = typeof meta.legs ({ shape, events, meta }, ref) => {
type C = T['toFixed'] type T = typeof meta.legs
type C = T['toFixed']
return ( return (
<SVGContainer ref={ref}> <SVGContainer ref={ref}>
<g {...events}> <g {...events}>
<rect width={shape.size[0]} height={shape.size[1]} fill="none" stroke="black" /> <rect width={shape.size[0]} height={shape.size[1]} fill="none" stroke="black" />
</g> </g>
</SVGContainer> </SVGContainer>
) )
} }
)
Indicator: TLIndicator<BoxShape, SVGSVGElement, Meta> = ({ shape }) => { Indicator: TLIndicator<BoxShape, SVGSVGElement, Meta> = ({ shape }) => {
return ( return (
@ -93,7 +95,7 @@ describe('When creating a minimal ShapeUtil', () => {
render(<H message="Hello" />) render(<H message="Hello" />)
render( render(
<Box._Component <Box.Component
ref={ref} ref={ref}
shape={boxShape} shape={boxShape}
isEditing={false} isEditing={false}
@ -129,18 +131,20 @@ describe('When creating a realistic API around TLShapeUtil', () => {
age = 100 age = 100
Component: TLComponent<BoxShape, SVGSVGElement, Meta> = ({ shape, events, meta }, ref) => { Component = React.forwardRef<SVGSVGElement, TLComponentProps<BoxShape, SVGSVGElement>>(
type T = typeof meta.legs ({ shape, events, meta }, ref) => {
type C = T['toFixed'] type T = typeof meta.legs
type C = T['toFixed']
return ( return (
<SVGContainer ref={ref}> <SVGContainer ref={ref}>
<g {...events}> <g {...events}>
<rect width={shape.size[0]} height={shape.size[1]} fill="none" stroke="black" /> <rect width={shape.size[0]} height={shape.size[1]} fill="none" stroke="black" />
</g> </g>
</SVGContainer> </SVGContainer>
) )
} }
)
Indicator: TLIndicator<BoxShape, SVGSVGElement, Meta> = ({ shape }) => { Indicator: TLIndicator<BoxShape, SVGSVGElement, Meta> = ({ shape }) => {
return ( return (
@ -214,7 +218,7 @@ describe('When creating a realistic API around TLShapeUtil', () => {
render(<H message="Hello" />) render(<H message="Hello" />)
render( render(
<Box._Component <Box.Component
ref={ref} ref={ref}
shape={box} shape={box}
isEditing={false} isEditing={false}

View file

@ -2,15 +2,7 @@
import * as React from 'react' import * as React from 'react'
import Utils from '+utils' import Utils from '+utils'
import { intersectPolylineBounds } from '@tldraw/intersect' import { intersectPolylineBounds } from '@tldraw/intersect'
import type { TLBounds, TLForwardedRef, TLComponentProps, TLShape } from 'types' import type { TLBounds, TLComponentProps, TLShape } from 'types'
export interface TLComponent<T extends TLShape, E extends Element = any, M = any> {
(
this: TLShapeUtil<T, E, M>,
props: TLComponentProps<T, E, M>,
ref: TLForwardedRef<E>
): React.ReactElement<TLComponentProps<T, E, M>, E['tagName']> | null
}
export interface TLIndicator<T extends TLShape, E extends Element = any, M = any> { export interface TLIndicator<T extends TLShape, E extends Element = any, M = any> {
( (
@ -20,12 +12,6 @@ export interface TLIndicator<T extends TLShape, E extends Element = any, M = any
} }
export abstract class TLShapeUtil<T extends TLShape, E extends Element = any, M = any> { export abstract class TLShapeUtil<T extends TLShape, E extends Element = any, M = any> {
_Component: React.ForwardRefExoticComponent<any>
constructor() {
this._Component = React.forwardRef(this.Component)
}
refMap = new Map<string, React.RefObject<E>>() refMap = new Map<string, React.RefObject<E>>()
boundsCache = new WeakMap<TLShape, TLBounds>() boundsCache = new WeakMap<TLShape, TLBounds>()
@ -42,9 +28,9 @@ export abstract class TLShapeUtil<T extends TLShape, E extends Element = any, M
isAspectRatioLocked = false isAspectRatioLocked = false
Component: TLComponent<T, E, M> = () => null abstract Component: React.ForwardRefExoticComponent<TLComponentProps<T, E, M>>
Indicator: TLIndicator<T, E, M> = () => null abstract Indicator: TLIndicator<T, E, M>
shouldRender: (prev: T, next: T) => boolean = () => true shouldRender: (prev: T, next: T) => boolean = () => true

View file

@ -71,7 +71,7 @@ export interface TLShape {
isAspectRatioLocked?: boolean isAspectRatioLocked?: boolean
} }
export interface TLComponentProps<T extends TLShape, E = any, M = any> { export type TLComponentProps<T extends TLShape, E = any, M = any> = {
shape: T shape: T
isEditing: boolean isEditing: boolean
isBinding: boolean isBinding: boolean
@ -88,7 +88,7 @@ export interface TLComponentProps<T extends TLShape, E = any, M = any> {
onPointerMove: (e: React.PointerEvent<E>) => void onPointerMove: (e: React.PointerEvent<E>) => void
onPointerLeave: (e: React.PointerEvent<E>) => void onPointerLeave: (e: React.PointerEvent<E>) => void
} }
} } & React.RefAttributes<E>
export interface TLShapeProps<T extends TLShape, E = any, M = any> export interface TLShapeProps<T extends TLShape, E = any, M = any>
extends TLComponentProps<T, E, M> { extends TLComponentProps<T, E, M> {

View file

@ -2,108 +2,123 @@
/* refresh-reset */ /* refresh-reset */
import * as React from 'react' import * as React from 'react'
import { TLShape, Utils, TLBounds, ShapeUtil, HTMLContainer } from '@tldraw/core' import {
TLShape,
Utils,
TLBounds,
TLShapeUtil,
HTMLContainer,
TLComponent,
SVGContainer,
TLIndicator,
} from '@tldraw/core'
// Define a custom shape // Define a custom shape
export interface RectangleShape extends TLShape { export interface BoxShape extends TLShape {
type: 'rectangle' type: 'box'
size: number[] size: number[]
text: string text: string
} }
export const boxShape: BoxShape = {
id: 'example1',
type: 'box',
parentId: 'page',
childIndex: 0,
name: 'Example Shape',
point: [0, 0],
size: [100, 100],
rotation: 0,
text: 'Hello world!',
}
// Create a "shape utility" class that interprets that shape // Create a "shape utility" class that interprets that shape
export const Rectangle = new ShapeUtil<RectangleShape, HTMLDivElement, { isDarkMode: boolean }>( export class BoxUtil extends TLShapeUtil<BoxShape, HTMLDivElement> {
() => ({ age = 100
type: 'rectangle',
defaultProps: {
id: 'example1',
type: 'rectangle',
parentId: 'page1',
childIndex: 0,
name: 'Example Shape',
point: [0, 0],
size: [100, 100],
rotation: 0,
text: 'Hello world!',
},
Component({ shape, events, meta, onShapeChange, isEditing }, ref) {
const color = meta.isDarkMode ? 'white' : 'black'
const rInput = React.useRef<HTMLDivElement>(null) Component: TLComponent<BoxShape, HTMLDivElement> = (
{ shape, events, onShapeChange, isEditing, meta },
ref
) => {
console.log('hi')
const color = meta.isDarkMode ? 'white' : 'black'
function updateShapeSize() { const rInput = React.useRef<HTMLDivElement>(null)
const elm = rInput.current!
onShapeChange?.({ function updateShapeSize() {
...shape, const elm = rInput.current!
text: elm.innerText,
size: [elm.offsetWidth + 44, elm.offsetHeight + 44], onShapeChange?.({
}) ...shape,
text: elm.innerText,
size: [elm.offsetWidth + 44, elm.offsetHeight + 44],
})
}
React.useLayoutEffect(() => {
const elm = rInput.current!
const observer = new MutationObserver(updateShapeSize)
observer.observe(elm, {
attributes: true,
characterData: true,
subtree: true,
})
elm.innerText = shape.text
updateShapeSize()
return () => {
observer.disconnect()
} }
}, [])
React.useLayoutEffect(() => { React.useEffect(() => {
const elm = rInput.current! if (isEditing) {
rInput.current!.focus()
}
}, [isEditing])
const observer = new MutationObserver(updateShapeSize) return (
<HTMLContainer ref={ref}>
observer.observe(elm, { <div
attributes: true, {...events}
characterData: true, style={{
subtree: true, pointerEvents: 'all',
}) width: shape.size[0],
height: shape.size[1],
elm.innerText = shape.text display: 'flex',
updateShapeSize() fontSize: 20,
fontFamily: 'sans-serif',
return () => { alignItems: 'center',
observer.disconnect() justifyContent: 'center',
} border: `2px solid ${color}`,
}, []) color,
}}
React.useEffect(() => { >
if (isEditing) { <div onPointerDown={(e) => isEditing && e.stopPropagation()}>
rInput.current!.focus() <div
} ref={rInput}
}, [isEditing]) style={{
whiteSpace: 'nowrap',
return ( overflow: 'hidden',
<HTMLContainer ref={ref}> textAlign: 'center',
<div outline: 'none',
{...events} userSelect: isEditing ? 'all' : 'none',
style={{ }}
pointerEvents: 'all', contentEditable={isEditing}
width: shape.size[0], />
height: shape.size[1],
display: 'flex',
fontSize: 20,
fontFamily: 'sans-serif',
alignItems: 'center',
justifyContent: 'center',
border: `2px solid ${color}`,
color,
}}
>
<div onPointerDown={(e) => isEditing && e.stopPropagation()}>
<div
ref={rInput}
style={{
whiteSpace: 'nowrap',
overflow: 'hidden',
textAlign: 'center',
outline: 'none',
userSelect: isEditing ? 'all' : 'none',
}}
contentEditable={isEditing}
/>
</div>
</div> </div>
</HTMLContainer> </div>
) </HTMLContainer>
}, )
Indicator({ shape }) { }
return (
Indicator: TLIndicator<BoxShape> = ({ shape }) => {
return (
<SVGContainer>
<rect <rect
fill="none" fill="none"
stroke="blue" stroke="blue"
@ -112,22 +127,23 @@ export const Rectangle = new ShapeUtil<RectangleShape, HTMLDivElement, { isDarkM
height={shape.size[1]} height={shape.size[1]}
pointerEvents="none" pointerEvents="none"
/> />
) </SVGContainer>
}, )
getBounds(shape) { }
const bounds = Utils.getFromCache(this.boundsCache, shape, () => {
const [width, height] = shape.size
return {
minX: 0,
maxX: width,
minY: 0,
maxY: height,
width,
height,
} as TLBounds
})
return Utils.translateBounds(bounds, shape.point) getBounds = (shape: BoxShape) => {
}, const bounds = Utils.getFromCache(this.boundsCache, shape, () => {
}) const [width, height] = shape.size
) return {
minX: 0,
maxX: width,
minY: 0,
maxY: height,
width,
height,
} as TLBounds
})
return Utils.translateBounds(bounds, shape.point)
}
}

View file

@ -1,12 +1,12 @@
import * as React from 'react' import * as React from 'react'
import { Renderer } from '@tldraw/core' import { Renderer, TLShapeUtilsMap } from '@tldraw/core'
import { Rectangle } from './box' import { BoxShape, BoxUtil } from './box'
import { Label } from './label' import { LabelUtil, LabelShape } from './label'
import { appState } from './state' import { appState } from './state'
const shapeUtils: any = { const shapeUtils: TLShapeUtilsMap<BoxShape | LabelShape> = {
rectangle: Rectangle, box: new BoxUtil(),
label: Label, label: new LabelUtil(),
} }
export default function Core() { export default function Core() {

View file

@ -2,7 +2,15 @@
/* refresh-reset */ /* refresh-reset */
import * as React from 'react' import * as React from 'react'
import { TLShape, Utils, TLBounds, ShapeUtil, HTMLContainer } from '@tldraw/core' import {
TLShape,
Utils,
TLBounds,
HTMLContainer,
TLComponent,
TLShapeUtil,
TLIndicator,
} from '@tldraw/core'
import { appState } from './state' import { appState } from './state'
// Define a custom shape // Define a custom shape
@ -14,53 +22,37 @@ export interface LabelShape extends TLShape {
// Create a "shape utility" class that interprets that shape // Create a "shape utility" class that interprets that shape
export const Label = new ShapeUtil<LabelShape, HTMLDivElement, { isDarkMode: boolean }>(() => ({ export class LabelUtil extends TLShapeUtil<LabelShape, HTMLDivElement> {
type: 'label', type = 'label' as const
isStateful: true, isStateful = true
defaultProps: { Component: TLComponent<LabelShape, HTMLDivElement> = (
id: 'example1', { shape, events, isSelected, onShapeChange, meta },
type: 'label', ref
parentId: 'page1', ) => {
childIndex: 0,
name: 'Example Shape',
point: [0, 0],
rotation: 0,
text: 'Hello world!',
},
Component({ shape, events, meta, onShapeChange, isSelected }, ref) {
const color = meta.isDarkMode ? 'white' : 'black' const color = meta.isDarkMode ? 'white' : 'black'
const bounds = this.getBounds(shape) const bounds = this.getBounds(shape)
const rInput = React.useRef<HTMLDivElement>(null) const rInput = React.useRef<HTMLDivElement>(null)
function updateShapeSize() { function updateShapeSize() {
const elm = rInput.current! const elm = rInput.current!
appState.changeShapeText(shape.id, elm.innerText) appState.changeShapeText(shape.id, elm.innerText)
onShapeChange?.({ onShapeChange?.({
id: shape.id, id: shape.id,
text: elm.innerText, text: elm.innerText,
}) })
} }
React.useLayoutEffect(() => { React.useLayoutEffect(() => {
const elm = rInput.current! const elm = rInput.current!
elm.innerText = shape.text elm.innerText = shape.text
updateShapeSize() updateShapeSize()
const observer = new MutationObserver(updateShapeSize) const observer = new MutationObserver(updateShapeSize)
observer.observe(elm, { observer.observe(elm, {
attributes: true, attributes: true,
characterData: true, characterData: true,
subtree: true, subtree: true,
}) })
}, []) }, [])
return ( return (
<HTMLContainer> <HTMLContainer>
<div <div
@ -94,9 +86,10 @@ export const Label = new ShapeUtil<LabelShape, HTMLDivElement, { isDarkMode: boo
</div> </div>
</HTMLContainer> </HTMLContainer>
) )
}, }
Indicator({ shape }) {
const bounds = this?.getBounds(shape) Indicator: TLIndicator<LabelShape> = ({ shape }) => {
const bounds = this.getBounds(shape)
return ( return (
<rect <rect
@ -108,8 +101,9 @@ export const Label = new ShapeUtil<LabelShape, HTMLDivElement, { isDarkMode: boo
pointerEvents="none" pointerEvents="none"
/> />
) )
}, }
getBounds(shape) {
getBounds = (shape: LabelShape) => {
const bounds = Utils.getFromCache(this.boundsCache, shape, () => { const bounds = Utils.getFromCache(this.boundsCache, shape, () => {
const ref = this.getRef(shape) const ref = this.getRef(shape)
const width = ref.current?.offsetWidth || 0 const width = ref.current?.offsetWidth || 0
@ -126,5 +120,5 @@ export const Label = new ShapeUtil<LabelShape, HTMLDivElement, { isDarkMode: boo
}) })
return Utils.translateBounds(bounds, shape.point) return Utils.translateBounds(bounds, shape.point)
}, }
})) }

View file

@ -5,11 +5,11 @@ import type {
TLPointerEventHandler, TLPointerEventHandler,
TLShapeChangeHandler, TLShapeChangeHandler,
} from '@tldraw/core' } from '@tldraw/core'
import type { RectangleShape } from './box' import type { BoxShape } from './box'
import type { LabelShape } from './label' import type { LabelShape } from './label'
import { StateManager } from 'rko' import { StateManager } from 'rko'
type Shapes = RectangleShape | LabelShape type Shapes = BoxShape | LabelShape
interface State { interface State {
page: TLPage<Shapes, TLBinding> page: TLPage<Shapes, TLBinding>
@ -89,7 +89,7 @@ class AppState extends StateManager<State> {
} }
onShapeChange: TLShapeChangeHandler<Shapes> = (shape) => { onShapeChange: TLShapeChangeHandler<Shapes> = (shape) => {
if (shape.type === 'rectangle' && shape.size) { if (shape.type === 'box' && shape.size) {
this.patchState({ this.patchState({
page: { page: {
shapes: { shapes: {
@ -110,7 +110,7 @@ export const appState = new AppState({
parentId: 'page1', parentId: 'page1',
name: 'Rectangle', name: 'Rectangle',
childIndex: 1, childIndex: 1,
type: 'rectangle', type: 'box',
point: [0, 0], point: [0, 0],
rotation: 0, rotation: 0,
size: [100, 100], size: [100, 100],

View file

@ -6,7 +6,7 @@ import {
TLBinding, TLBinding,
TLBounds, TLBounds,
TLIndicator, TLIndicator,
TLComponent, TLComponentProps,
TLPointerInfo, TLPointerInfo,
} from '@tldraw/core' } from '@tldraw/core'
import { Vec } from '@tldraw/vec' import { Vec } from '@tldraw/vec'
@ -86,7 +86,7 @@ export class ArrowUtil extends TLDrawShapeUtil<T, E> {
) )
} }
Component: TLComponent<T, E> = ({ shape, meta, events }, ref) => { Component = React.forwardRef<E, TLComponentProps<T, E, M>>(({ shape, meta, events }, ref) => {
const { const {
handles: { start, bend, end }, handles: { start, bend, end },
decorations = {}, decorations = {},
@ -262,7 +262,7 @@ export class ArrowUtil extends TLDrawShapeUtil<T, E> {
</g> </g>
</SVGContainer> </SVGContainer>
) )
} })
Indicator: TLIndicator<T> = ({ shape }) => { Indicator: TLIndicator<T> = ({ shape }) => {
const path = getArrowPath(shape) const path = getArrowPath(shape)

View file

@ -1,5 +1,5 @@
import * as React from 'react' import * as React from 'react'
import { Utils, SVGContainer, TLBounds, TLIndicator, TLComponent } from '@tldraw/core' import { Utils, SVGContainer, TLBounds, TLIndicator, TLComponentProps } from '@tldraw/core'
import { Vec } from '@tldraw/vec' import { Vec } from '@tldraw/vec'
import { getStrokeOutlinePoints, getStrokePoints, StrokeOptions } from 'perfect-freehand' import { getStrokeOutlinePoints, getStrokePoints, StrokeOptions } from 'perfect-freehand'
import { defaultStyle, getShapeStyle } from '../shape-styles' import { defaultStyle, getShapeStyle } from '../shape-styles'
@ -39,7 +39,7 @@ export class DrawUtil extends TLDrawShapeUtil<T, E> {
) )
} }
Component: TLComponent<T, E> = ({ shape, meta, events }, ref) => { Component = React.forwardRef<E, TLComponentProps<T, E>>(({ shape, meta, events }, ref) => {
const { points, style, isComplete } = shape const { points, style, isComplete } = shape
const polygonPathData = React.useMemo(() => { const polygonPathData = React.useMemo(() => {
@ -144,7 +144,7 @@ export class DrawUtil extends TLDrawShapeUtil<T, E> {
/> />
</SVGContainer> </SVGContainer>
) )
} })
Indicator: TLIndicator<T> = ({ shape }) => { Indicator: TLIndicator<T> = ({ shape }) => {
const { points } = shape const { points } = shape

View file

@ -1,5 +1,5 @@
import * as React from 'react' import * as React from 'react'
import { Utils, SVGContainer, TLIndicator, TLComponent, TLBounds } from '@tldraw/core' import { Utils, SVGContainer, TLIndicator, TLComponentProps, TLBounds } from '@tldraw/core'
import { Vec } from '@tldraw/vec' import { Vec } from '@tldraw/vec'
import { getStrokeOutlinePoints, getStrokePoints } from 'perfect-freehand' import { getStrokeOutlinePoints, getStrokePoints } from 'perfect-freehand'
import { defaultStyle, getShapeStyle } from '../shape-styles' import { defaultStyle, getShapeStyle } from '../shape-styles'
@ -33,23 +33,65 @@ export class EllipseUtil extends TLDrawShapeUtil<T, E> {
) )
} }
Component: TLComponent<T, E> = ({ shape, isBinding, meta, events }, ref) => { Component = React.forwardRef<E, TLComponentProps<T, E>>(
const { ({ shape, isBinding, meta, events }, ref) => {
radius: [radiusX, radiusY], const {
style, radius: [radiusX, radiusY],
} = shape style,
} = shape
const styles = getShapeStyle(style, meta.isDarkMode) const styles = getShapeStyle(style, meta.isDarkMode)
const strokeWidth = +styles.strokeWidth const strokeWidth = +styles.strokeWidth
const sw = 1 + strokeWidth * 1.618 const sw = 1 + strokeWidth * 1.618
const rx = Math.max(0, radiusX - sw / 2) const rx = Math.max(0, radiusX - sw / 2)
const ry = Math.max(0, radiusY - sw / 2) const ry = Math.max(0, radiusY - sw / 2)
if (style.dash === DashStyle.Draw) { if (style.dash === DashStyle.Draw) {
const path = getEllipsePath(shape, this.getCenter(shape)) const path = getEllipsePath(shape, this.getCenter(shape))
return (
<SVGContainer ref={ref} id={shape.id + '_svg'} {...events}>
{isBinding && (
<ellipse
className="tl-binding-indicator"
cx={radiusX}
cy={radiusY}
rx={rx + 2}
ry={ry + 2}
/>
)}
<path
d={getEllipseIndicatorPathData(shape, this.getCenter(shape))}
stroke="none"
fill={style.isFilled ? styles.fill : 'none'}
pointerEvents="all"
/>
<path
d={path}
fill={styles.stroke}
stroke={styles.stroke}
strokeWidth={styles.strokeWidth}
pointerEvents="all"
strokeLinecap="round"
strokeLinejoin="round"
/>
</SVGContainer>
)
}
const h = Math.pow(rx - ry, 2) / Math.pow(rx + ry, 2)
const perimeter = Math.PI * (rx + ry) * (1 + (3 * h) / (10 + Math.sqrt(4 - 3 * h)))
const { strokeDasharray, strokeDashoffset } = Utils.getPerfectDashProps(
perimeter < 64 ? perimeter * 2 : perimeter,
strokeWidth * 1.618,
shape.style.dash,
4
)
return ( return (
<SVGContainer ref={ref} id={shape.id + '_svg'} {...events}> <SVGContainer ref={ref} id={shape.id + '_svg'} {...events}>
@ -58,21 +100,20 @@ export class EllipseUtil extends TLDrawShapeUtil<T, E> {
className="tl-binding-indicator" className="tl-binding-indicator"
cx={radiusX} cx={radiusX}
cy={radiusY} cy={radiusY}
rx={rx + 2} rx={rx + 32}
ry={ry + 2} ry={ry + 32}
/> />
)} )}
<path <ellipse
d={getEllipseIndicatorPathData(shape, this.getCenter(shape))} cx={radiusX}
stroke="none" cy={radiusY}
fill={style.isFilled ? styles.fill : 'none'} rx={rx}
pointerEvents="all" ry={ry}
/> fill={styles.fill}
<path
d={path}
fill={styles.stroke}
stroke={styles.stroke} stroke={styles.stroke}
strokeWidth={styles.strokeWidth} strokeWidth={sw}
strokeDasharray={strokeDasharray}
strokeDashoffset={strokeDashoffset}
pointerEvents="all" pointerEvents="all"
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
@ -80,46 +121,7 @@ export class EllipseUtil extends TLDrawShapeUtil<T, E> {
</SVGContainer> </SVGContainer>
) )
} }
)
const h = Math.pow(rx - ry, 2) / Math.pow(rx + ry, 2)
const perimeter = Math.PI * (rx + ry) * (1 + (3 * h) / (10 + Math.sqrt(4 - 3 * h)))
const { strokeDasharray, strokeDashoffset } = Utils.getPerfectDashProps(
perimeter < 64 ? perimeter * 2 : perimeter,
strokeWidth * 1.618,
shape.style.dash,
4
)
return (
<SVGContainer ref={ref} id={shape.id + '_svg'} {...events}>
{isBinding && (
<ellipse
className="tl-binding-indicator"
cx={radiusX}
cy={radiusY}
rx={rx + 32}
ry={ry + 32}
/>
)}
<ellipse
cx={radiusX}
cy={radiusY}
rx={rx}
ry={ry}
fill={styles.fill}
stroke={styles.stroke}
strokeWidth={sw}
strokeDasharray={strokeDasharray}
strokeDashoffset={strokeDashoffset}
pointerEvents="all"
strokeLinecap="round"
strokeLinejoin="round"
/>
</SVGContainer>
)
}
Indicator: TLIndicator<T> = ({ shape }) => { Indicator: TLIndicator<T> = ({ shape }) => {
return <path d={getEllipseIndicatorPathData(shape, this.getCenter(shape))} /> return <path d={getEllipseIndicatorPathData(shape, this.getCenter(shape))} />

View file

@ -1,5 +1,5 @@
import * as React from 'react' import * as React from 'react'
import { Utils, SVGContainer, TLIndicator, TLComponent } from '@tldraw/core' import { Utils, SVGContainer, TLIndicator, TLComponentProps } from '@tldraw/core'
import { defaultStyle } from '../shape-styles' import { defaultStyle } from '../shape-styles'
import { TLDrawShapeType, GroupShape, ColorStyle } from '~types' import { TLDrawShapeType, GroupShape, ColorStyle } from '~types'
import { getBoundsRectangle } from '../shared' import { getBoundsRectangle } from '../shared'
@ -33,48 +33,57 @@ export class GroupUtil extends TLDrawShapeUtil<T, E> {
) )
} }
Component: TLComponent<T, E> = ({ shape, isBinding, isHovered, isSelected, events }, ref) => { Component = React.forwardRef<E, TLComponentProps<T, E>>(
const { id, size } = shape ({ shape, isBinding, isHovered, isSelected, events }, ref) => {
const { id, size } = shape
const sw = 2 const sw = 2
const w = Math.max(0, size[0] - sw / 2) const w = Math.max(0, size[0] - sw / 2)
const h = Math.max(0, size[1] - sw / 2) const h = Math.max(0, size[1] - sw / 2)
const strokes: [number[], number[], number][] = [ const strokes: [number[], number[], number][] = [
[[sw / 2, sw / 2], [w, sw / 2], w - sw / 2], [[sw / 2, sw / 2], [w, sw / 2], w - sw / 2],
[[w, sw / 2], [w, h], h - sw / 2], [[w, sw / 2], [w, h], h - sw / 2],
[[w, h], [sw / 2, h], w - sw / 2], [[w, h], [sw / 2, h], w - sw / 2],
[[sw / 2, h], [sw / 2, sw / 2], h - sw / 2], [[sw / 2, h], [sw / 2, sw / 2], h - sw / 2],
] ]
const paths = strokes.map(([start, end], i) => { const paths = strokes.map(([start, end], i) => {
return <line key={id + '_' + i} x1={start[0]} y1={start[1]} x2={end[0]} y2={end[1]} /> return <line key={id + '_' + i} x1={start[0]} y1={start[1]} x2={end[0]} y2={end[1]} />
}) })
return ( return (
<SVGContainer ref={ref} {...events}> <SVGContainer ref={ref} {...events}>
{isBinding && ( {isBinding && (
<rect
className="tl-binding-indicator"
x={-BINDING_DISTANCE}
y={-BINDING_DISTANCE}
width={size[0] + BINDING_DISTANCE * 2}
height={size[1] + BINDING_DISTANCE * 2}
/>
)}
<rect <rect
className="tl-binding-indicator" x={0}
x={-BINDING_DISTANCE} y={0}
y={-BINDING_DISTANCE} width={size[0]}
width={size[0] + BINDING_DISTANCE * 2} height={size[1]}
height={size[1] + BINDING_DISTANCE * 2} fill="transparent"
pointerEvents="all"
/> />
)} <g
<rect x={0} y={0} width={size[0]} height={size[1]} fill="transparent" pointerEvents="all" /> className={scaledLines()}
<g stroke={ColorStyle.Black}
className={scaledLines()} opacity={isHovered || isSelected ? 1 : 0}
stroke={ColorStyle.Black} strokeLinecap="round"
opacity={isHovered || isSelected ? 1 : 0} pointerEvents="stroke"
strokeLinecap="round" >
pointerEvents="stroke" {paths}
> </g>
{paths} </SVGContainer>
</g> )
</SVGContainer> }
) )
}
Indicator: TLIndicator<T> = ({ shape }) => { Indicator: TLIndicator<T> = ({ shape }) => {
const { id, size } = shape const { id, size } = shape

View file

@ -1,5 +1,5 @@
import * as React from 'react' import * as React from 'react'
import { Utils, SVGContainer, TLIndicator, TLComponent } from '@tldraw/core' import { Utils, SVGContainer, TLIndicator, TLComponentProps } from '@tldraw/core'
import { Vec } from '@tldraw/vec' import { Vec } from '@tldraw/vec'
import { getStroke, getStrokePoints } from 'perfect-freehand' import { getStroke, getStrokePoints } from 'perfect-freehand'
import { defaultStyle, getShapeStyle } from '../shape-styles' import { defaultStyle, getShapeStyle } from '../shape-styles'
@ -33,103 +33,105 @@ export class RectangleUtil extends TLDrawShapeUtil<T, E> {
) )
} }
Component: TLComponent<T, E> = ({ shape, isBinding, meta, events }, ref) => { Component = React.forwardRef<E, TLComponentProps<T, E>>(
const { id, size, style } = shape ({ shape, isBinding, meta, events }, ref) => {
const styles = getShapeStyle(style, meta.isDarkMode) const { id, size, style } = shape
const strokeWidth = +styles.strokeWidth const styles = getShapeStyle(style, meta.isDarkMode)
const strokeWidth = +styles.strokeWidth
if (style.dash === DashStyle.Draw) { if (style.dash === DashStyle.Draw) {
const pathData = getRectanglePath(shape) const pathData = getRectanglePath(shape)
return (
<SVGContainer ref={ref} id={shape.id + '_svg'} {...events}>
{isBinding && (
<rect
className="tl-binding-indicator"
x={strokeWidth / 2 - BINDING_DISTANCE}
y={strokeWidth / 2 - BINDING_DISTANCE}
width={Math.max(0, size[0] - strokeWidth / 2) + BINDING_DISTANCE * 2}
height={Math.max(0, size[1] - strokeWidth / 2) + BINDING_DISTANCE * 2}
/>
)}
<path
d={getRectangleIndicatorPathData(shape)}
fill={style.isFilled ? styles.fill : 'none'}
radius={strokeWidth}
stroke="none"
pointerEvents="all"
/>
<path
d={pathData}
fill={styles.stroke}
stroke={styles.stroke}
strokeWidth={styles.strokeWidth}
pointerEvents="all"
/>
</SVGContainer>
)
}
const sw = 1 + strokeWidth * 1.618
const w = Math.max(0, size[0] - sw / 2)
const h = Math.max(0, size[1] - sw / 2)
const strokes: [number[], number[], number][] = [
[[sw / 2, sw / 2], [w, sw / 2], w - sw / 2],
[[w, sw / 2], [w, h], h - sw / 2],
[[w, h], [sw / 2, h], w - sw / 2],
[[sw / 2, h], [sw / 2, sw / 2], h - sw / 2],
]
const paths = strokes.map(([start, end, length], i) => {
const { strokeDasharray, strokeDashoffset } = Utils.getPerfectDashProps(
length,
strokeWidth * 1.618,
shape.style.dash
)
return (
<line
key={id + '_' + i}
x1={start[0]}
y1={start[1]}
x2={end[0]}
y2={end[1]}
stroke={styles.stroke}
strokeWidth={sw}
strokeLinecap="round"
strokeDasharray={strokeDasharray}
strokeDashoffset={strokeDashoffset}
/>
)
})
return ( return (
<SVGContainer ref={ref} id={shape.id + '_svg'} {...events}> <SVGContainer ref={ref} id={shape.id + '_svg'} {...events}>
{isBinding && ( {isBinding && (
<rect <rect
className="tl-binding-indicator" className="tl-binding-indicator"
x={strokeWidth / 2 - BINDING_DISTANCE} x={sw / 2 - 32}
y={strokeWidth / 2 - BINDING_DISTANCE} y={sw / 2 - 32}
width={Math.max(0, size[0] - strokeWidth / 2) + BINDING_DISTANCE * 2} width={w + 64}
height={Math.max(0, size[1] - strokeWidth / 2) + BINDING_DISTANCE * 2} height={h + 64}
/> />
)} )}
<path <rect
d={getRectangleIndicatorPathData(shape)} x={sw / 2}
fill={style.isFilled ? styles.fill : 'none'} y={sw / 2}
radius={strokeWidth} width={w}
height={h}
fill={styles.fill}
stroke="none" stroke="none"
strokeWidth={sw}
pointerEvents="all" pointerEvents="all"
/> />
<path <g pointerEvents="stroke">{paths}</g>
d={pathData}
fill={styles.stroke}
stroke={styles.stroke}
strokeWidth={styles.strokeWidth}
pointerEvents="all"
/>
</SVGContainer> </SVGContainer>
) )
} }
)
const sw = 1 + strokeWidth * 1.618
const w = Math.max(0, size[0] - sw / 2)
const h = Math.max(0, size[1] - sw / 2)
const strokes: [number[], number[], number][] = [
[[sw / 2, sw / 2], [w, sw / 2], w - sw / 2],
[[w, sw / 2], [w, h], h - sw / 2],
[[w, h], [sw / 2, h], w - sw / 2],
[[sw / 2, h], [sw / 2, sw / 2], h - sw / 2],
]
const paths = strokes.map(([start, end, length], i) => {
const { strokeDasharray, strokeDashoffset } = Utils.getPerfectDashProps(
length,
strokeWidth * 1.618,
shape.style.dash
)
return (
<line
key={id + '_' + i}
x1={start[0]}
y1={start[1]}
x2={end[0]}
y2={end[1]}
stroke={styles.stroke}
strokeWidth={sw}
strokeLinecap="round"
strokeDasharray={strokeDasharray}
strokeDashoffset={strokeDashoffset}
/>
)
})
return (
<SVGContainer ref={ref} id={shape.id + '_svg'} {...events}>
{isBinding && (
<rect
className="tl-binding-indicator"
x={sw / 2 - 32}
y={sw / 2 - 32}
width={w + 64}
height={h + 64}
/>
)}
<rect
x={sw / 2}
y={sw / 2}
width={w}
height={h}
fill={styles.fill}
stroke="none"
strokeWidth={sw}
pointerEvents="all"
/>
<g pointerEvents="stroke">{paths}</g>
</SVGContainer>
)
}
Indicator: TLIndicator<T> = ({ shape }) => { Indicator: TLIndicator<T> = ({ shape }) => {
const { const {

View file

@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */ /* eslint-disable @typescript-eslint/no-non-null-assertion */
import * as React from 'react' import * as React from 'react'
import { Utils, HTMLContainer, TLBounds, TLIndicator, TLComponent } from '@tldraw/core' import { Utils, HTMLContainer, TLBounds, TLIndicator, TLComponentProps } from '@tldraw/core'
import { defaultStyle } from '../shape-styles' import { defaultStyle } from '../shape-styles'
import { StickyShape, TLDrawShapeType, TLDrawTransformInfo } from '~types' import { StickyShape, TLDrawShapeType, TLDrawTransformInfo } from '~types'
import { getBoundsRectangle, TextAreaUtils } from '../shared' import { getBoundsRectangle, TextAreaUtils } from '../shared'
@ -35,164 +35,163 @@ export class StickyUtil extends TLDrawShapeUtil<T, E> {
) )
} }
Component: TLComponent<T, E> = ( Component = React.forwardRef<E, TLComponentProps<T, E>>(
{ shape, meta, events, isEditing, onShapeBlur, onShapeChange }, ({ shape, meta, events, isEditing, onShapeBlur, onShapeChange }, ref) => {
ref const font = getStickyFontStyle(shape.style)
) => {
const font = getStickyFontStyle(shape.style)
const { color, fill } = getStickyShapeStyle(shape.style, meta.isDarkMode) const { color, fill } = getStickyShapeStyle(shape.style, meta.isDarkMode)
const rContainer = React.useRef<HTMLDivElement>(null) const rContainer = React.useRef<HTMLDivElement>(null)
const rTextArea = React.useRef<HTMLTextAreaElement>(null) const rTextArea = React.useRef<HTMLTextAreaElement>(null)
const rText = React.useRef<HTMLDivElement>(null) const rText = React.useRef<HTMLDivElement>(null)
const rIsMounted = React.useRef(false) const rIsMounted = React.useRef(false)
const handlePointerDown = React.useCallback((e: React.PointerEvent) => { const handlePointerDown = React.useCallback((e: React.PointerEvent) => {
e.stopPropagation() e.stopPropagation()
}, []) }, [])
const handleTextChange = React.useCallback( const handleTextChange = React.useCallback(
(e: React.ChangeEvent<HTMLTextAreaElement>) => { (e: React.ChangeEvent<HTMLTextAreaElement>) => {
onShapeChange?.({ onShapeChange?.({
id: shape.id, id: shape.id,
type: shape.type, type: shape.type,
text: normalizeText(e.currentTarget.value), text: normalizeText(e.currentTarget.value),
}) })
}, },
[onShapeChange] [onShapeChange]
) )
const handleKeyDown = React.useCallback( const handleKeyDown = React.useCallback(
(e: React.KeyboardEvent<HTMLTextAreaElement>) => { (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Escape') return if (e.key === 'Escape') return
if (e.key === 'Tab' && shape.text.length === 0) { if (e.key === 'Tab' && shape.text.length === 0) {
e.preventDefault() e.preventDefault()
return
}
e.stopPropagation()
if (e.key === 'Tab') {
e.preventDefault()
if (e.shiftKey) {
TextAreaUtils.unindent(e.currentTarget)
} else {
TextAreaUtils.indent(e.currentTarget)
}
onShapeChange?.({ ...shape, text: normalizeText(e.currentTarget.value) })
}
},
[shape, onShapeChange]
)
const handleBlur = React.useCallback(
(e: React.FocusEvent<HTMLTextAreaElement>) => {
if (!isEditing) return
if (rIsMounted.current) {
e.currentTarget.setSelectionRange(0, 0)
onShapeBlur?.()
}
},
[isEditing]
)
const handleFocus = React.useCallback(
(e: React.FocusEvent<HTMLTextAreaElement>) => {
if (!isEditing) return
if (!rIsMounted.current) return
if (document.activeElement === e.currentTarget) {
e.currentTarget.select()
}
},
[isEditing]
)
// Focus when editing changes to true
React.useEffect(() => {
if (isEditing) {
if (document.activeElement !== rText.current) {
requestAnimationFrame(() => {
rIsMounted.current = true
const elm = rTextArea.current!
elm.focus()
elm.select()
})
}
}
}, [isEditing])
// Resize to fit text
React.useEffect(() => {
const text = rText.current!
const { size } = shape
const { offsetHeight: currTextHeight } = text
const minTextHeight = MIN_CONTAINER_HEIGHT - PADDING * 2
const prevTextHeight = size[1] - PADDING * 2
// Same size? We can quit here
if (currTextHeight === prevTextHeight) return
if (currTextHeight > minTextHeight) {
// Snap the size to the text content if the text only when the
// text is larger than the minimum text height.
onShapeChange?.({ id: shape.id, size: [size[0], currTextHeight + PADDING * 2] })
return return
} }
e.stopPropagation() if (currTextHeight < minTextHeight && size[1] > MIN_CONTAINER_HEIGHT) {
// If we're smaller than the minimum height and the container
if (e.key === 'Tab') { // is too tall, snap it down to the minimum container height
e.preventDefault() onShapeChange?.({ id: shape.id, size: [size[0], MIN_CONTAINER_HEIGHT] })
if (e.shiftKey) { return
TextAreaUtils.unindent(e.currentTarget)
} else {
TextAreaUtils.indent(e.currentTarget)
}
onShapeChange?.({ ...shape, text: normalizeText(e.currentTarget.value) })
} }
}, }, [shape.text, shape.size[1], shape.style])
[shape, onShapeChange]
)
const handleBlur = React.useCallback( const style = {
(e: React.FocusEvent<HTMLTextAreaElement>) => { font,
if (!isEditing) return color,
if (rIsMounted.current) { textShadow: meta.isDarkMode
e.currentTarget.setSelectionRange(0, 0) ? `0.5px 0.5px 2px rgba(255, 255, 255,.25)`
onShapeBlur?.() : `0.5px 0.5px 2px rgba(255, 255, 255,.5)`,
}
},
[isEditing]
)
const handleFocus = React.useCallback(
(e: React.FocusEvent<HTMLTextAreaElement>) => {
if (!isEditing) return
if (!rIsMounted.current) return
if (document.activeElement === e.currentTarget) {
e.currentTarget.select()
}
},
[isEditing]
)
// Focus when editing changes to true
React.useEffect(() => {
if (isEditing) {
if (document.activeElement !== rText.current) {
requestAnimationFrame(() => {
rIsMounted.current = true
const elm = rTextArea.current!
elm.focus()
elm.select()
})
}
}
}, [isEditing])
// Resize to fit text
React.useEffect(() => {
const text = rText.current!
const { size } = shape
const { offsetHeight: currTextHeight } = text
const minTextHeight = MIN_CONTAINER_HEIGHT - PADDING * 2
const prevTextHeight = size[1] - PADDING * 2
// Same size? We can quit here
if (currTextHeight === prevTextHeight) return
if (currTextHeight > minTextHeight) {
// Snap the size to the text content if the text only when the
// text is larger than the minimum text height.
onShapeChange?.({ id: shape.id, size: [size[0], currTextHeight + PADDING * 2] })
return
} }
if (currTextHeight < minTextHeight && size[1] > MIN_CONTAINER_HEIGHT) { return (
// If we're smaller than the minimum height and the container <HTMLContainer ref={ref} {...events}>
// is too tall, snap it down to the minimum container height <div
onShapeChange?.({ id: shape.id, size: [size[0], MIN_CONTAINER_HEIGHT] }) ref={rContainer}
return className={styledStickyContainer({ isDarkMode: meta.isDarkMode })}
} style={{ backgroundColor: fill, ...style }}
}, [shape.text, shape.size[1], shape.style]) >
<div ref={rText} className={styledText({ isEditing })}>
const style = { {shape.text}&#8203;
font, </div>
color, {isEditing && (
textShadow: meta.isDarkMode <textarea
? `0.5px 0.5px 2px rgba(255, 255, 255,.25)` ref={rTextArea}
: `0.5px 0.5px 2px rgba(255, 255, 255,.5)`, className={styledTextArea({ isEditing })}
} onPointerDown={handlePointerDown}
value={shape.text}
return ( onChange={handleTextChange}
<HTMLContainer ref={ref} {...events}> onKeyDown={handleKeyDown}
<div onFocus={handleFocus}
ref={rContainer} onBlur={handleBlur}
className={styledStickyContainer({ isDarkMode: meta.isDarkMode })} autoCapitalize="off"
style={{ backgroundColor: fill, ...style }} autoComplete="off"
> spellCheck={false}
<div ref={rText} className={styledText({ isEditing })}> autoFocus
{shape.text}&#8203; />
)}
</div> </div>
{isEditing && ( </HTMLContainer>
<textarea )
ref={rTextArea} }
className={styledTextArea({ isEditing })} )
onPointerDown={handlePointerDown}
value={shape.text}
onChange={handleTextChange}
onKeyDown={handleKeyDown}
onFocus={handleFocus}
onBlur={handleBlur}
autoCapitalize="off"
autoComplete="off"
spellCheck={false}
autoFocus
/>
)}
</div>
</HTMLContainer>
)
}
Indicator: TLIndicator<T> = ({ shape }) => { Indicator: TLIndicator<T> = ({ shape }) => {
const { const {

View file

@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */ /* eslint-disable @typescript-eslint/no-non-null-assertion */
import * as React from 'react' import * as React from 'react'
import { Utils, HTMLContainer, TLIndicator, TLComponent, TLBounds } from '@tldraw/core' import { Utils, HTMLContainer, TLIndicator, TLComponentProps, TLBounds } from '@tldraw/core'
import { defaultStyle, getShapeStyle, getFontStyle } from '../shape-styles' import { defaultStyle, getShapeStyle, getFontStyle } from '../shape-styles'
import { TextShape, TLDrawShapeType, TLDrawTransformInfo } from '~types' import { TextShape, TLDrawShapeType, TLDrawTransformInfo } from '~types'
import { TextAreaUtils } from '../shared' import { TextAreaUtils } from '../shared'
@ -38,148 +38,147 @@ export class TextUtil extends TLDrawShapeUtil<T, E> {
) )
} }
Component: TLComponent<T, E> = ( Component = React.forwardRef<E, TLComponentProps<T, E>>(
{ shape, isBinding, isEditing, onShapeBlur, onShapeChange, meta, events }, ({ shape, isBinding, isEditing, onShapeBlur, onShapeChange, meta, events }, ref) => {
ref const rInput = React.useRef<HTMLTextAreaElement>(null)
) => { const { text, style } = shape
const rInput = React.useRef<HTMLTextAreaElement>(null) const styles = getShapeStyle(style, meta.isDarkMode)
const { text, style } = shape const font = getFontStyle(shape.style)
const styles = getShapeStyle(style, meta.isDarkMode)
const font = getFontStyle(shape.style)
const rIsMounted = React.useRef(false) const rIsMounted = React.useRef(false)
const handleChange = React.useCallback( const handleChange = React.useCallback(
(e: React.ChangeEvent<HTMLTextAreaElement>) => { (e: React.ChangeEvent<HTMLTextAreaElement>) => {
onShapeChange?.({ ...shape, text: normalizeText(e.currentTarget.value) }) onShapeChange?.({ ...shape, text: normalizeText(e.currentTarget.value) })
}, },
[shape] [shape]
) )
const handleKeyDown = React.useCallback( const handleKeyDown = React.useCallback(
(e: React.KeyboardEvent<HTMLTextAreaElement>) => { (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
e.stopPropagation() e.stopPropagation()
if (e.key === 'Escape' || (e.key === 'Enter' && (e.ctrlKey || e.metaKey))) { if (e.key === 'Escape' || (e.key === 'Enter' && (e.ctrlKey || e.metaKey))) {
e.currentTarget.blur() e.currentTarget.blur()
return return
}
if (e.key === 'Tab') {
e.preventDefault()
if (e.shiftKey) {
TextAreaUtils.unindent(e.currentTarget)
} else {
TextAreaUtils.indent(e.currentTarget)
} }
onShapeChange?.({ ...shape, text: normalizeText(e.currentTarget.value) }) if (e.key === 'Tab') {
} e.preventDefault()
}, if (e.shiftKey) {
[shape, onShapeChange] TextAreaUtils.unindent(e.currentTarget)
) } else {
TextAreaUtils.indent(e.currentTarget)
}
const handleBlur = React.useCallback( onShapeChange?.({ ...shape, text: normalizeText(e.currentTarget.value) })
(e: React.FocusEvent<HTMLTextAreaElement>) => { }
if (!isEditing) return },
if (rIsMounted.current) { [shape, onShapeChange]
e.currentTarget.setSelectionRange(0, 0) )
onShapeBlur?.()
}
},
[isEditing]
)
const handleFocus = React.useCallback( const handleBlur = React.useCallback(
(e: React.FocusEvent<HTMLTextAreaElement>) => { (e: React.FocusEvent<HTMLTextAreaElement>) => {
if (!isEditing) return if (!isEditing) return
if (!rIsMounted.current) return if (rIsMounted.current) {
e.currentTarget.setSelectionRange(0, 0)
onShapeBlur?.()
}
},
[isEditing]
)
if (document.activeElement === e.currentTarget) { const handleFocus = React.useCallback(
e.currentTarget.select() (e: React.FocusEvent<HTMLTextAreaElement>) => {
} if (!isEditing) return
}, if (!rIsMounted.current) return
[isEditing]
)
const handlePointerDown = React.useCallback( if (document.activeElement === e.currentTarget) {
(e) => { e.currentTarget.select()
}
},
[isEditing]
)
const handlePointerDown = React.useCallback(
(e) => {
if (isEditing) {
e.stopPropagation()
}
},
[isEditing]
)
React.useEffect(() => {
if (isEditing) { if (isEditing) {
e.stopPropagation() requestAnimationFrame(() => {
rIsMounted.current = true
const elm = rInput.current!
elm.focus()
elm.select()
})
} }
}, }, [isEditing])
[isEditing]
)
React.useEffect(() => { return (
if (isEditing) { <HTMLContainer ref={ref} {...events}>
requestAnimationFrame(() => { <div className={wrapper({ isEditing })} onPointerDown={handlePointerDown}>
rIsMounted.current = true <div
const elm = rInput.current! className={innerWrapper()}
elm.focus() style={{
elm.select() font,
}) color: styles.stroke,
} }}
}, [isEditing]) >
{isBinding && (
return ( <div
<HTMLContainer ref={ref} {...events}> className="tl-binding-indicator"
<div className={wrapper({ isEditing })} onPointerDown={handlePointerDown}> style={{
<div position: 'absolute',
className={innerWrapper()} top: -BINDING_DISTANCE,
style={{ left: -BINDING_DISTANCE,
font, width: `calc(100% + ${BINDING_DISTANCE * 2}px)`,
color: styles.stroke, height: `calc(100% + ${BINDING_DISTANCE * 2}px)`,
}} backgroundColor: 'var(--tl-selectFill)',
> }}
{isBinding && ( />
<div )}
className="tl-binding-indicator" {isEditing ? (
style={{ <textarea
position: 'absolute', className={textArea({ isBinding })}
top: -BINDING_DISTANCE, ref={rInput}
left: -BINDING_DISTANCE, style={{
width: `calc(100% + ${BINDING_DISTANCE * 2}px)`, font,
height: `calc(100% + ${BINDING_DISTANCE * 2}px)`, color: styles.stroke,
backgroundColor: 'var(--tl-selectFill)', }}
}} name="text"
/> defaultValue={text}
)} tabIndex={-1}
{isEditing ? ( autoComplete="false"
<textarea autoCapitalize="false"
className={textArea({ isBinding })} autoCorrect="false"
ref={rInput} autoSave="false"
style={{ placeholder=""
font, color={styles.stroke}
color: styles.stroke, onFocus={handleFocus}
}} onBlur={handleBlur}
name="text" onChange={handleChange}
defaultValue={text} onKeyDown={handleKeyDown}
tabIndex={-1} onPointerDown={handlePointerDown}
autoComplete="false" autoFocus
autoCapitalize="false" wrap="off"
autoCorrect="false" dir="auto"
autoSave="false" datatype="wysiwyg"
placeholder="" />
color={styles.stroke} ) : (
onFocus={handleFocus} text
onBlur={handleBlur} )}
onChange={handleChange} </div>
onKeyDown={handleKeyDown}
onPointerDown={handlePointerDown}
autoFocus
wrap="off"
dir="auto"
datatype="wysiwyg"
/>
) : (
text
)}
</div> </div>
</div> </HTMLContainer>
</HTMLContainer> )
) }
} )
Indicator: TLIndicator<T> = ({ shape }) => { Indicator: TLIndicator<T> = ({ shape }) => {
const { width, height } = this.getBounds(shape) const { width, height } = this.getBounds(shape)