Fix rendering bug, tweak API
This commit is contained in:
parent
a906a3bd95
commit
c04e4134d2
15 changed files with 664 additions and 653 deletions
|
@ -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}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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)
|
||||||
},
|
}
|
||||||
}))
|
}
|
||||||
|
|
|
@ -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],
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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))} />
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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}​
|
||||||
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}​
|
/>
|
||||||
|
)}
|
||||||
</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 {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in a new issue