Fix text scrolling

This commit is contained in:
Steve Ruiz 2021-09-11 23:17:54 +01:00
parent c8c3ebce68
commit c004ed5e56
24 changed files with 305 additions and 416 deletions

View file

@ -51,11 +51,20 @@ export function Canvas<T extends TLShape, M extends Record<string, unknown>>({
const rLayer = useCameraCss(rContainer, pageState)
const preventScrolling = React.useCallback((e: React.UIEvent<HTMLDivElement, UIEvent>) => {
e.currentTarget.scrollTo(0, 0)
}, [])
return (
<div className="tl-container" ref={rContainer}>
<div id="canvas" className="tl-absolute tl-canvas" ref={rCanvas} {...events}>
<div
id="canvas"
className="tl-absolute tl-canvas"
ref={rCanvas}
onScroll={preventScrolling}
{...events}
>
<ErrorBoundary FallbackComponent={ErrorFallback} onReset={resetError}>
{/* <Defs zoom={pageState.camera.zoom} /> */}
<div ref={rLayer} className="tl-absolute tl-layer">
<Page
page={page}

View file

@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import * as React from 'react'
import type { TLBinding, TLPage, TLPageState, TLShape } from '+types'
import { useSelection, useShapeTree, useHandles, useRenderOnResize, useTLContext } from '+hooks'
import { useSelection, useShapeTree, useHandles, useTLContext } from '+hooks'
import { Bounds } from '+components/bounds'
import { BoundsBg } from '+components/bounds/bounds-bg'
import { Handles } from '+components/handles'
@ -30,7 +30,14 @@ export function Page<T extends TLShape, M extends Record<string, unknown>>({
}: PageProps<T, M>): JSX.Element {
const { callbacks, shapeUtils, inputs } = useTLContext()
const shapeTree = useShapeTree(page, pageState, shapeUtils, inputs.size, meta, callbacks.onChange)
const shapeTree = useShapeTree(
page,
pageState,
shapeUtils,
inputs.size,
meta,
callbacks.onRenderCountChange
)
const { shapeWithHandles } = useHandles(page, pageState)

View file

@ -15,9 +15,9 @@ import { useTLTheme, TLContext, TLContextType } from '../../hooks'
export interface RendererProps<
T extends TLShape,
E extends HTMLElement | SVGElement,
E extends Element,
M extends Record<string, unknown>
> extends Partial<TLCallbacks> {
> extends Partial<TLCallbacks<T>> {
/**
* An object containing instances of your shape classes.
*/
@ -66,11 +66,7 @@ export interface RendererProps<
* @param props
* @returns
*/
export function Renderer<
T extends TLShape,
E extends SVGElement | HTMLElement,
M extends Record<string, unknown>
>({
export function Renderer<T extends TLShape, E extends Element, M extends Record<string, unknown>>({
shapeUtils,
page,
pageState,
@ -91,6 +87,8 @@ export function Renderer<
rPageState.current = pageState
}, [pageState])
rest
const [context] = React.useState<TLContextType<T, E>>(() => ({
callbacks: rest,
shapeUtils,
@ -100,7 +98,7 @@ export function Renderer<
}))
return (
<TLContext.Provider value={context as TLContextType<T, E>}>
<TLContext.Provider value={context as unknown as TLContextType<TLShape, Element>}>
<Canvas
page={page}
pageState={pageState}

View file

@ -1,53 +0,0 @@
import { useTLContext } from '+hooks'
import * as React from 'react'
import type { TLShapeUtil, TLRenderInfo, TLShape } from '+types'
export function EditingTextShape<
T extends TLShape,
E extends SVGElement | HTMLElement,
M extends Record<string, unknown>
>({
shape,
utils,
isEditing,
isBinding,
isHovered,
isSelected,
isCurrentParent,
events,
meta,
}: TLRenderInfo<M, E> & {
shape: T
utils: TLShapeUtil<T, E>
}) {
const {
callbacks: { onTextChange, onTextBlur, onTextFocus, onTextKeyDown, onTextKeyUp },
} = useTLContext()
const ref = utils.getRef(shape)
React.useEffect(() => {
// Firefox fix?
setTimeout(() => {
if (document.activeElement !== ref.current) {
ref.current?.focus()
}
}, 0)
}, [shape.id])
return (
<utils.render
ref={ref}
{...{
shape,
isEditing,
isHovered,
isSelected,
isCurrentParent,
isBinding,
meta,
events: { ...events, onTextChange, onTextBlur, onTextFocus, onTextKeyDown, onTextKeyUp },
}}
/>
)
}

View file

@ -3,7 +3,7 @@ import * as React from 'react'
import type { TLShapeUtil, TLRenderInfo, TLShape } from '+types'
export const RenderedShape = React.memo(
<T extends TLShape, E extends SVGElement | HTMLElement, M extends Record<string, unknown>>({
<T extends TLShape, E extends Element, M extends Record<string, unknown>>({
shape,
utils,
isEditing,
@ -11,9 +11,11 @@ export const RenderedShape = React.memo(
isHovered,
isSelected,
isCurrentParent,
onShapeChange,
onShapeBlur,
events,
meta,
}: TLRenderInfo<M, E> & {
}: TLRenderInfo<T, M, E> & {
shape: T
utils: TLShapeUtil<T, E>
}) => {
@ -22,16 +24,16 @@ export const RenderedShape = React.memo(
return (
<utils.render
ref={ref}
{...{
shape,
isEditing,
isBinding,
isHovered,
isSelected,
isCurrentParent,
meta,
events,
}}
shape={shape}
isEditing={isEditing}
isBinding={isBinding}
isHovered={isHovered}
isSelected={isSelected}
isCurrentParent={isCurrentParent}
meta={meta}
events={events}
onShapeChange={onShapeChange}
onShapeBlur={onShapeBlur}
/>
)
},

View file

@ -13,7 +13,7 @@ export const ShapeNode = React.memo(
isSelected,
isCurrentParent,
meta,
}: { utils: TLShapeUtils<TLShape, HTMLElement | SVGElement> } & IShapeTreeNode<TLShape, any>) => {
}: { utils: TLShapeUtils<TLShape, Element> } & IShapeTreeNode<TLShape, any>) => {
return (
<>
<Shape

View file

@ -18,5 +18,5 @@ describe('shape', () => {
})
})
// { shape: TLShape; ref: ForwardedRef<HTMLElement | SVGElement>; } & TLRenderInfo<any, any> & RefAttributes<HTMLElement | SVGElement>
// { shape: TLShape; ref: ForwardedRef<Element>; } & TLRenderInfo<any, any> & RefAttributes<Element>
// { shape: BoxShape; ref: ForwardedRef<any>; } & TLRenderInfo<any, any> & RefAttributes<any>'

View file

@ -1,10 +1,11 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import * as React from 'react'
import { useShapeEvents } from '+hooks'
import type { IShapeTreeNode, TLShape, TLShapeUtil } from '+types'
import { RenderedShape } from './rendered-shape'
import { EditingTextShape } from './editing-text-shape'
import { Container } from '+components/container'
import { useTLContext } from '+hooks'
// function setTransform(elm: HTMLDivElement, bounds: TLBounds, rotation = 0) {
// const transform = `
@ -16,11 +17,7 @@ import { Container } from '+components/container'
// elm.style.setProperty('height', `calc(${bounds.height}px + (var(--tl-padding) * 2))`)
// }
export const Shape = <
T extends TLShape,
E extends SVGElement | HTMLElement,
M extends Record<string, unknown>
>({
export const Shape = <T extends TLShape, E extends Element, M extends Record<string, unknown>>({
shape,
utils,
isEditing,
@ -32,6 +29,7 @@ export const Shape = <
}: IShapeTreeNode<T, M> & {
utils: TLShapeUtil<T, E>
}) => {
const { callbacks } = useTLContext()
const bounds = utils.getBounds(shape)
const events = useShapeEvents(shape.id, isCurrentParent)
@ -42,31 +40,18 @@ export const Shape = <
bounds={bounds}
rotation={shape.rotation}
>
{isEditing && utils.isEditableText ? (
<EditingTextShape
shape={shape}
isBinding={false}
isCurrentParent={false}
isEditing={true}
isHovered={isHovered}
isSelected={isSelected}
utils={utils as any}
meta={meta as any}
events={events}
/>
) : (
<RenderedShape
shape={shape}
isBinding={isBinding}
isCurrentParent={isCurrentParent}
isEditing={isEditing}
isHovered={isHovered}
isSelected={isSelected}
utils={utils as any}
meta={meta as any}
events={events}
/>
)}
<RenderedShape
shape={shape}
isBinding={isBinding}
isCurrentParent={isCurrentParent}
isEditing={isEditing}
isHovered={isHovered}
isSelected={isSelected}
utils={utils as any}
meta={meta as any}
events={events}
onShapeChange={callbacks.onShapeChange}
/>
</Container>
)
}

View file

@ -8,7 +8,6 @@ export * from './useStyle'
export * from './useCanvasEvents'
export * from './useBoundsHandleEvents'
export * from './useCameraCss'
export * from './useRenderOnResize'
export * from './useSelection'
export * from './useHandleEvents'
export * from './useHandles'

View file

@ -1,14 +0,0 @@
import * as React from 'react'
import Utils from '+utils'
export function useRenderOnResize() {
const forceUpdate = React.useReducer((x) => x + 1, 0)[1]
React.useEffect(() => {
const debouncedUpdate = Utils.debounce(forceUpdate, 96)
window.addEventListener('resize', debouncedUpdate)
return () => {
window.removeEventListener('resize', debouncedUpdate)
}
}, [forceUpdate])
}

View file

@ -2,7 +2,7 @@ import { useTLContext } from '+hooks'
import * as React from 'react'
import { Utils } from '+utils'
export function useResizeObserver<T extends HTMLElement | SVGElement>(ref: React.RefObject<T>) {
export function useResizeObserver<T extends Element>(ref: React.RefObject<T>) {
const { inputs } = useTLContext()
const rIsMounted = React.useRef(false)
const forceUpdate = React.useReducer((x) => x + 1, 0)[1]

View file

@ -9,7 +9,7 @@ export function useSafariFocusOutFix(): void {
useEffect(() => {
function handleFocusOut() {
callbacks.onBlurEditingShape?.()
callbacks.onShapeBlur?.()
}
if (Utils.isMobileSafari()) {

View file

@ -6,7 +6,7 @@ function canvasToScreen(point: number[], camera: TLPageState['camera']): number[
return [(point[0] + camera.point[0]) * camera.zoom, (point[1] + camera.point[1]) * camera.zoom]
}
export function useSelection<T extends TLShape, E extends HTMLElement | SVGElement>(
export function useSelection<T extends TLShape, E extends Element>(
page: TLPage<T, TLBinding>,
pageState: TLPageState,
shapeUtils: TLShapeUtils<T, E>

View file

@ -61,7 +61,7 @@ function shapeIsInViewport(bounds: TLBounds, viewport: TLBounds) {
export function useShapeTree<
T extends TLShape,
E extends SVGElement | HTMLElement,
E extends Element,
M extends Record<string, unknown>
>(
page: TLPage<T, TLBinding>,
@ -69,7 +69,7 @@ export function useShapeTree<
shapeUtils: TLShapeUtils<T, E>,
size: number[],
meta?: M,
onChange?: TLCallbacks['onChange']
onRenderCountChange?: TLCallbacks<T>['onRenderCountChange']
) {
const rTimeout = React.useRef<unknown>()
const rPreviousCount = React.useRef(0)
@ -128,7 +128,7 @@ export function useShapeTree<
clearTimeout(rTimeout.current as number)
}
rTimeout.current = setTimeout(() => {
onChange?.(Array.from(shapesIdsToRender.values()))
onRenderCountChange?.(Array.from(shapesIdsToRender.values()))
}, 100)
rPreviousCount.current = shapesToRender.size
}

View file

@ -115,11 +115,16 @@ const tlcss = css`
--tl-camera-y: 0px;
--tl-padding: calc(64px * var(--tl-scale));
position: relative;
box-sizing: border-box;
top: 0px;
left: 0px;
width: 100%;
height: 100%;
max-width: 100%;
max-height: 100%;
box-sizing: border-box;
padding: 0px;
margin: 0px;
z-index: 100;
touch-action: none;
overscroll-behavior: none;
background-color: var(--tl-background);
@ -130,6 +135,25 @@ const tlcss = css`
box-sizing: border-box;
}
.tl-canvas {
position: absolute;
overflow: hidden;
width: 100%;
height: 100%;
touch-action: none;
pointer-events: all;
}
.tl-layer {
position: absolute;
top: 0;
left: 0;
height: 0;
width: 0;
transform: scale(var(--tl-zoom), var(--tl-zoom))
translate(var(--tl-camera-x), var(--tl-camera-y));
}
.tl-absolute {
position: absolute;
top: 0px;
@ -141,6 +165,7 @@ const tlcss = css`
position: absolute;
top: 0px;
left: 0px;
overflow: hidden;
transform-origin: center center;
pointer-events: none;
display: flex;
@ -151,22 +176,17 @@ const tlcss = css`
.tl-positioned-svg {
width: 100%;
height: 100%;
overflow: hidden;
}
.tl-positioned-div {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
padding: var(--tl-padding);
}
.tl-layer {
transform: scale(var(--tl-zoom), var(--tl-zoom))
translate(var(--tl-camera-x), var(--tl-camera-y));
height: 0;
width: 0;
}
.tl-counter-scaled {
transform: scale(var(--tl-scale));
}
@ -253,14 +273,6 @@ const tlcss = css`
pointer-events: none;
}
.tl-canvas {
overflow: hidden;
width: 100%;
height: 100%;
touch-action: none;
pointer-events: all;
}
.tl-dot {
fill: var(--tl-background);
stroke: var(--tl-foreground);

View file

@ -2,18 +2,16 @@ import * as React from 'react'
import type { Inputs } from '+inputs'
import type { TLCallbacks, TLShape, TLBounds, TLPageState, TLShapeUtils } from '+types'
export interface TLContextType<T extends TLShape, E extends HTMLElement | SVGElement> {
export interface TLContextType<T extends TLShape, E extends Element> {
id?: string
callbacks: Partial<TLCallbacks>
callbacks: Partial<TLCallbacks<T>>
shapeUtils: TLShapeUtils<T, E>
rPageState: React.MutableRefObject<TLPageState>
rScreenBounds: React.MutableRefObject<TLBounds | null>
inputs: Inputs
}
export const TLContext = React.createContext<TLContextType<any, any>>(
{} as TLContextType<TLShape, HTMLElement | SVGElement>
)
export const TLContext = React.createContext({} as TLContextType<TLShape, Element>)
export function useTLContext() {
const context = React.useContext(TLContext)

View file

@ -6,7 +6,7 @@ import Utils, { Vec } from '+utils'
import { useGesture } from '@use-gesture/react'
// Capture zoom gestures (pinches, wheels and pans)
export function useZoomEvents<T extends HTMLElement | SVGElement>(ref: React.RefObject<T>) {
export function useZoomEvents<T extends Element>(ref: React.RefObject<T>) {
const rOriginPoint = React.useRef<number[] | undefined>(undefined)
const rPinchPoint = React.useRef<number[] | undefined>(undefined)
const rDelta = React.useRef<number[]>([0, 0])

View file

@ -57,33 +57,27 @@ export interface TLShape {
isAspectRatioLocked?: boolean
}
export type TLShapeUtils<T extends TLShape, E extends SVGElement | HTMLElement> = Record<
string,
TLShapeUtil<T, E>
>
export type TLShapeUtils<T extends TLShape, E extends Element> = Record<string, TLShapeUtil<T, E>>
export interface TLRenderInfo<M = any, E = any> {
export interface TLRenderInfo<T extends TLShape, M = any, E = any> {
isEditing: boolean
isBinding: boolean
isHovered: boolean
isSelected: boolean
isCurrentParent: boolean
meta: M extends any ? M : never
onShapeChange?: TLCallbacks<T>['onShapeChange']
onShapeBlur?: TLCallbacks<T>['onShapeBlur']
events: {
onPointerDown: (e: React.PointerEvent<E>) => void
onPointerUp: (e: React.PointerEvent<E>) => void
onPointerEnter: (e: React.PointerEvent<E>) => void
onPointerMove: (e: React.PointerEvent<E>) => void
onPointerLeave: (e: React.PointerEvent<E>) => void
onTextChange?: TLCallbacks['onTextChange']
onTextBlur?: TLCallbacks['onTextBlur']
onTextFocus?: TLCallbacks['onTextFocus']
onTextKeyDown?: TLCallbacks['onTextKeyDown']
onTextKeyUp?: TLCallbacks['onTextKeyUp']
}
}
export interface TLShapeProps<T extends TLShape, E = any, M = any> extends TLRenderInfo<M, E> {
export interface TLShapeProps<T extends TLShape, E = any, M = any> extends TLRenderInfo<T, M, E> {
ref: ForwardedRef<E>
shape: T
}
@ -131,9 +125,7 @@ export type TLBoundsHandleEventHandler = (
e: React.PointerEvent
) => void
export interface TLCallbacks {
onChange: (ids: string[]) => void
export interface TLCallbacks<T extends TLShape> {
// Camera events
onPinchStart: TLPinchEventHandler
onPinchEnd: TLPinchEventHandler
@ -189,15 +181,10 @@ export interface TLCallbacks {
onUnhoverHandle: TLPointerEventHandler
onReleaseHandle: TLPointerEventHandler
// Text
onTextChange: (id: string, text: string) => void
onTextBlur: (id: string) => void
onTextFocus: (id: string) => void
onTextKeyDown: (id: string, key: string) => void
onTextKeyUp: (id: string, key: string) => void
// Misc
onBlurEditingShape: () => void
onRenderCountChange: (ids: string[]) => void
onShapeChange: (shape: { id: string } & Partial<T>) => void
onShapeBlur: () => void
onError: (error: Error) => void
}
@ -278,7 +265,7 @@ export interface TLBezierCurveSegment {
/* Shape Utility */
/* -------------------------------------------------- */
export abstract class TLShapeUtil<T extends TLShape, E extends HTMLElement | SVGElement> {
export abstract class TLShapeUtil<T extends TLShape, E extends Element> {
refMap = new Map<string, React.RefObject<E>>()
boundsCache = new WeakMap<TLShape, TLBounds>()
@ -296,7 +283,7 @@ export abstract class TLShapeUtil<T extends TLShape, E extends HTMLElement | SVG
abstract defaultProps: T
abstract render: React.ForwardRefExoticComponent<
{ shape: T; ref: React.ForwardedRef<E> } & TLRenderInfo & React.RefAttributes<E>
{ shape: T; ref: React.ForwardedRef<E> } & TLRenderInfo<T> & React.RefAttributes<E>
>
abstract renderIndicator(shape: T): JSX.Element | null

View file

@ -2,7 +2,7 @@ import * as React from 'react'
import { IdProvider } from '@radix-ui/react-id'
import { Renderer } from '@tldraw/core'
import styled from '~styles'
import { Data, TLDrawDocument, TLDrawStatus, TLDrawToolType } from '~types'
import { Data, TLDrawDocument, TLDrawStatus } from '~types'
import { TLDrawState } from '~state'
import { TLDrawContext, useCustomFonts, useKeyboardShortcuts, useTLDrawContext } from '~hooks'
import { tldrawShapeUtils } from '~shape'
@ -198,14 +198,10 @@ function InnerTldraw({
onHoverHandle={tlstate.onHoverHandle}
onUnhoverHandle={tlstate.onUnhoverHandle}
onReleaseHandle={tlstate.onReleaseHandle}
onChange={tlstate.onChange}
onError={tlstate.onError}
onBlurEditingShape={tlstate.onBlurEditingShape}
onTextBlur={tlstate.onTextBlur}
onTextChange={tlstate.onTextChange}
onTextKeyDown={tlstate.onTextKeyDown}
onTextFocus={tlstate.onTextFocus}
onTextKeyUp={tlstate.onTextKeyUp}
onRenderCountChange={tlstate.onRenderCountChange}
onShapeChange={tlstate.onShapeChange}
onShapeBlur={tlstate.onShapeBlur}
/>
</ContextMenu>
<MenuButtons>
@ -220,10 +216,14 @@ function InnerTldraw({
}
const Layout = styled('div', {
overflow: 'hidden',
position: 'absolute',
height: '100%',
width: '100%',
minHeight: 0,
minWidth: 0,
maxHeight: '100%',
maxWidth: '100%',
overflow: 'hidden',
padding: '8px 8px 0 8px',
display: 'flex',
alignItems: 'flex-start',
@ -241,9 +241,6 @@ const Layout = styled('div', {
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
zIndex: 100,
},
})

View file

@ -1,16 +1,15 @@
import * as React from 'react'
import {
SVGContainer,
TLBounds,
Utils,
Vec,
TLTransformInfo,
Intersect,
TLShapeProps,
} from '@tldraw/core'
import { SVGContainer, TLBounds, Utils, Vec, TLTransformInfo, Intersect } from '@tldraw/core'
import getStroke, { getStrokePoints } from 'perfect-freehand'
import { defaultStyle, getShapeStyle } from '~shape/shape-styles'
import { DrawShape, DashStyle, TLDrawShapeUtil, TLDrawShapeType, TLDrawToolType } from '~types'
import {
DrawShape,
DashStyle,
TLDrawShapeUtil,
TLDrawShapeType,
TLDrawToolType,
TLDrawShapeProps,
} from '~types'
export class Draw extends TLDrawShapeUtil<DrawShape, SVGSVGElement> {
type = TLDrawShapeType.Draw as const
@ -38,7 +37,7 @@ export class Draw extends TLDrawShapeUtil<DrawShape, SVGSVGElement> {
return next.points !== prev.points || next.style !== prev.style
}
render = React.forwardRef<SVGSVGElement, TLShapeProps<DrawShape, SVGSVGElement>>(
render = React.forwardRef<SVGSVGElement, TLDrawShapeProps<DrawShape, SVGSVGElement>>(
({ shape, meta, events, isEditing }, ref) => {
const { points, style } = shape

View file

@ -1,15 +1,15 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import * as React from 'react'
import { HTMLContainer, TLBounds, Utils, Vec, TLTransformInfo, Intersect } from '@tldraw/core'
import { getShapeStyle, getFontStyle, defaultStyle } from '~shape/shape-styles'
import {
SVGContainer,
TLBounds,
Utils,
Vec,
TLTransformInfo,
Intersect,
TLShapeProps,
} from '@tldraw/core'
import { getShapeStyle, getFontSize, getFontStyle, defaultStyle } from '~shape/shape-styles'
import { TextShape, TLDrawShapeUtil, TLDrawShapeType, TLDrawToolType, ArrowShape } from '~types'
TextShape,
TLDrawShapeUtil,
TLDrawShapeType,
TLDrawToolType,
ArrowShape,
TLDrawShapeProps,
} from '~types'
import styled from '~styles'
import TextAreaUtils from './text-utils'
@ -57,7 +57,7 @@ if (typeof window !== 'undefined') {
melm = getMeasurementDiv()
}
export class Text extends TLDrawShapeUtil<TextShape, SVGSVGElement> {
export class Text extends TLDrawShapeUtil<TextShape, HTMLDivElement> {
type = TLDrawShapeType.Text as const
toolType = TLDrawToolType.Text
isAspectRatioLocked = true
@ -91,118 +91,83 @@ export class Text extends TLDrawShapeUtil<TextShape, SVGSVGElement> {
)
}
render = React.forwardRef<SVGSVGElement, TLShapeProps<TextShape, SVGSVGElement>>(
({ shape, meta, isEditing, isBinding, events }, ref) => {
render = React.forwardRef<HTMLDivElement, TLDrawShapeProps<TextShape, HTMLDivElement>>(
({ shape, meta, isEditing, isBinding, onShapeChange, onShapeBlur, events }, ref) => {
const rInput = React.useRef<HTMLTextAreaElement>(null)
const { id, text, style } = shape
const { text, style } = shape
const styles = getShapeStyle(style, meta.isDarkMode)
const font = getFontStyle(shape.style)
const bounds = this.getBounds(shape)
function handleChange(e: React.ChangeEvent<HTMLTextAreaElement>) {
events.onTextChange?.(id, normalizeText(e.currentTarget.value))
}
const handleChange = React.useCallback(
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
onShapeChange?.({ ...shape, text: normalizeText(e.currentTarget.value) })
},
[shape]
)
function handleKeyDown(e: React.KeyboardEvent<HTMLTextAreaElement>) {
events.onTextKeyDown?.(id, e.key)
const handleKeyDown = React.useCallback(
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Escape') return
if (e.key === 'Escape') return
e.stopPropagation()
e.stopPropagation()
if (e.key === 'Tab') {
e.preventDefault()
if (e.shiftKey) {
TextAreaUtils.unindent(e.currentTarget)
} else {
TextAreaUtils.indent(e.currentTarget)
}
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]
)
events.onTextChange?.(id, normalizeText(e.currentTarget.value))
}
}
const handleBlur = React.useCallback(
(e: React.FocusEvent<HTMLTextAreaElement>) => {
e.currentTarget.setSelectionRange(0, 0)
onShapeBlur?.()
},
[isEditing, shape]
)
function handleKeyUp(e: React.KeyboardEvent<HTMLTextAreaElement>) {
events.onTextKeyUp?.(id, e.key)
}
const handleFocus = React.useCallback(
(e: React.FocusEvent<HTMLTextAreaElement>) => {
if (!isEditing) return
if (document.activeElement === e.currentTarget) {
e.currentTarget.select()
}
},
[isEditing]
)
function handleBlur(e: React.FocusEvent<HTMLTextAreaElement>) {
const handlePointerDown = React.useCallback(
(e) => {
if (isEditing) {
e.stopPropagation()
}
},
[isEditing]
)
React.useEffect(() => {
if (isEditing) {
e.currentTarget.focus()
e.currentTarget.select()
return
setTimeout(() => {
const elm = rInput.current!
elm.focus()
elm.select()
}, 0)
} else {
const elm = rInput.current!
elm.setSelectionRange(0, 0)
}
setTimeout(() => {
events.onTextBlur?.(id)
}, 0)
}
function handleFocus(e: React.FocusEvent<HTMLTextAreaElement>) {
if (document.activeElement === e.currentTarget) {
e.currentTarget.select()
events.onTextFocus?.(id)
}
}
function handlePointerDown() {
const elm = rInput.current
if (!elm) return
if (elm.selectionEnd !== 0) {
elm.selectionEnd = 0
}
}
const fontSize = getFontSize(shape.style.size) * (shape.style.scale || 1)
const lineHeight = fontSize * 1.3
if (!isEditing) {
return (
<SVGContainer ref={ref} {...events}>
{isBinding && (
<rect
className="tl-binding-indicator"
x={-16}
y={-16}
width={bounds.width + 32}
height={bounds.height + 32}
/>
)}
{text.split('\n').map((str, i) => (
<text
key={i}
x={4}
y={4 + fontSize / 2 + i * lineHeight}
fontFamily="Caveat Brush"
fontStyle="normal"
fontWeight="500"
letterSpacing={LETTER_SPACING}
fontSize={fontSize}
width={bounds.width}
height={bounds.height}
fill={styles.stroke}
color={styles.stroke}
stroke="none"
xmlSpace="preserve"
dominantBaseline="mathematical"
alignmentBaseline="mathematical"
>
{str}
</text>
))}
</SVGContainer>
)
}
}, [isEditing])
return (
<SVGContainer ref={ref} {...events}>
<foreignObject
width={bounds.width}
height={bounds.height}
pointerEvents="none"
onPointerDown={(e) => e.stopPropagation()}
>
<HTMLContainer ref={ref} {...events}>
<StyledWrapper isEditing={isEditing} onPointerDown={handlePointerDown}>
<StyledTextArea
ref={rInput}
style={{
@ -218,16 +183,21 @@ export class Text extends TLDrawShapeUtil<TextShape, SVGSVGElement> {
autoSave="false"
placeholder=""
color={styles.stroke}
autoFocus={true}
onFocus={handleFocus}
onBlur={handleBlur}
onKeyDown={handleKeyDown}
onKeyUp={handleKeyUp}
onChange={handleChange}
onKeyDown={handleKeyDown}
onPointerDown={handlePointerDown}
autoFocus={isEditing}
isEditing={isEditing}
isBinding={isBinding}
readOnly={!isEditing}
wrap="off"
dir="auto"
datatype="wysiwyg"
/>
</foreignObject>
</SVGContainer>
</StyledWrapper>
</HTMLContainer>
)
}
)
@ -252,7 +222,8 @@ export class Text extends TLDrawShapeUtil<TextShape, SVGSVGElement> {
melm.style.font = getFontStyle(shape.style)
// In tests, offsetWidth and offsetHeight will be 0
const [width, height] = [melm.offsetWidth || 1, melm.offsetHeight || 1]
const width = melm.offsetWidth || 1
const height = melm.offsetHeight || 1
return {
minX: 0,
@ -291,7 +262,7 @@ export class Text extends TLDrawShapeUtil<TextShape, SVGSVGElement> {
transform(
_shape: TextShape,
bounds: TLBounds,
{ initialShape, scaleX, scaleY, transformOrigin }: TLTransformInfo<TextShape>
{ initialShape, scaleX, scaleY }: TLTransformInfo<TextShape>
): Partial<TextShape> {
const {
rotation = 0,
@ -441,67 +412,30 @@ export class Text extends TLDrawShapeUtil<TextShape, SVGSVGElement> {
distance,
}
}
// getBindingPoint(shape, point, origin, direction, expandDistance) {
// const bounds = this.getBounds(shape)
// const expandedBounds = expandBounds(bounds, expandDistance)
// let bindingPoint: number[]
// let distance: number
// if (!HitTest.bounds(point, expandedBounds)) return
// // The point is inside of the box, so we'll assume the user is
// // indicating a specific point inside of the box.
// if (HitTest.bounds(point, bounds)) {
// bindingPoint = vec.divV(vec.sub(point, [expandedBounds.minX, expandedBounds.minY]), [
// expandedBounds.width,
// expandedBounds.height,
// ])
// distance = 0
// } else {
// // Find furthest intersection between ray from
// // origin through point and expanded bounds.
// const intersection = Intersect.ray
// .bounds(origin, direction, expandedBounds)
// .filter(int => int.didIntersect)
// .map(int => int.points[0])
// .sort((a, b) => vec.dist(b, origin) - vec.dist(a, origin))[0]
// // The anchor is a point between the handle and the intersection
// const anchor = vec.med(point, intersection)
// // Find the distance between the point and the real bounds of the shape
// const distanceFromShape = getBoundsSides(bounds)
// .map(side => vec.distanceToLineSegment(side[1][0], side[1][1], point))
// .sort((a, b) => a - b)[0]
// if (vec.distanceToLineSegment(point, anchor, this.getCenter(shape)) < 12) {
// // If we're close to the center, snap to the center
// bindingPoint = [0.5, 0.5]
// } else {
// // Or else calculate a normalized point
// bindingPoint = vec.divV(vec.sub(anchor, [expandedBounds.minX, expandedBounds.minY]), [
// expandedBounds.width,
// expandedBounds.height,
// ])
// }
// distance = distanceFromShape
// }
// return {
// point: bindingPoint,
// distance,
// }
// }
}
const StyledTextArea = styled('textarea', {
zIndex: 1,
const StyledWrapper = styled('div', {
width: '100%',
height: '100%',
variants: {
isEditing: {
false: {
pointerEvents: 'all',
},
true: {
pointerEvents: 'none',
},
},
},
})
const StyledTextArea = styled('textarea', {
position: 'absolute',
top: 'var(--tl-padding)',
left: 'var(--tl-padding)',
zIndex: 1,
width: 'calc(100% - (var(--tl-padding) * 2))',
height: 'calc(100% - (var(--tl-padding) * 2))',
border: 'none',
padding: '4px',
whiteSpace: 'pre',
@ -514,12 +448,46 @@ const StyledTextArea = styled('textarea', {
letterSpacing: LETTER_SPACING,
outline: 0,
fontWeight: '500',
backgroundColor: '$boundsBg',
overflow: 'hidden',
pointerEvents: 'all',
backfaceVisibility: 'hidden',
display: 'inline-block',
userSelect: 'text',
WebkitUserSelect: 'text',
WebkitTouchCallout: 'none',
variants: {
isBinding: {
false: {},
true: {
background: '$boundsBg',
},
},
isEditing: {
false: {
pointerEvents: 'none',
userSelect: 'none',
background: 'none',
WebkitUserSelect: 'none',
},
true: {
pointerEvents: 'all',
userSelect: 'text',
background: '$boundsBg',
WebkitUserSelect: 'text',
},
},
},
})
const NormalText = styled('div', {
display: 'block',
whiteSpace: 'pre',
alignmentBaseline: 'mathematical',
dominantBaseline: 'mathematical',
pointerEvents: 'none',
opacity: '0.5',
padding: '4px',
margin: '0',
outline: 0,
fontWeight: '500',
lineHeight: 1.4,
letterSpacing: LETTER_SPACING,
})

View file

@ -30,22 +30,24 @@ export class TextSession implements Session {
const { initialShape } = this
const pageId = data.appState.currentPageId
let nextShape: TextShape = {
...TLDR.getShape<TextShape>(data, initialShape.id, pageId),
text,
}
// let nextShape: TextShape = {
// ...TLDR.getShape<TextShape>(data, initialShape.id, pageId),
// text,
// }
nextShape = {
...nextShape,
...TLDR.getShapeUtils(nextShape).onStyleChange(nextShape),
} as TextShape
// nextShape = {
// ...nextShape,
// ...TLDR.getShapeUtils(nextShape).onStyleChange(nextShape),
// } as TextShape
return {
document: {
pages: {
[pageId]: {
shapes: {
[initialShape.id]: nextShape,
[initialShape.id]: {
text,
},
},
},
},

View file

@ -16,7 +16,6 @@ import {
TLPointerInfo,
inputs,
TLBounds,
Patch,
} from '@tldraw/core'
import {
FlipType,
@ -2234,11 +2233,11 @@ export class TLDrawState extends StateManager<Data> {
}
onPinchEnd: TLPinchEventHandler = () => {
if (this.state.settings.isZoomSnap) {
const i = Math.round((this.pageState.camera.zoom * 100) / 25)
const nextZoom = TLDR.getCameraZoom(i * 0.25)
this.zoomTo(nextZoom, inputs.pointer?.point)
}
// if (this.state.settings.isZoomSnap) {
// const i = Math.round((this.pageState.camera.zoom * 100) / 25)
// const nextZoom = TLDR.getCameraZoom(i * 0.25)
// this.zoomTo(nextZoom, inputs.pointer?.point)
// }
this.setStatus(TLDrawStatus.Idle)
}
@ -2405,7 +2404,7 @@ export class TLDrawState extends StateManager<Data> {
}
}
onDoubleClickCanvas: TLCanvasEventHandler = (info) => {
onDoubleClickCanvas: TLCanvasEventHandler = () => {
// Unused
switch (this.appState.status.current) {
case TLDrawStatus.Idle: {
@ -2704,31 +2703,24 @@ export class TLDrawState extends StateManager<Data> {
// Unused
}
onTextChange = (id: string, text: string) => {
this.updateTextSession(text)
onShapeChange = (shape: { id: string } & Partial<TLDrawShape>) => {
switch (shape.type) {
case TLDrawShapeType.Text: {
this.updateTextSession(shape.text || '')
}
}
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
onTextBlur = (id: string) => {
this.completeSession()
onShapeBlur = () => {
switch (this.appState.status.current) {
case TLDrawStatus.EditingText: {
this.completeSession()
}
}
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
onTextFocus = (id: string) => {
// Unused
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
onTextKeyDown = (id: string, key: string) => {
// Unused
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
onTextKeyUp = (id: string, key: string) => {
// Unused
}
onChange = (ids: string[]) => {
onRenderCountChange = (ids: string[]) => {
const appState = this.getAppState()
if (appState.isEmptyCanvas && ids.length > 0) {
this.patchState(
@ -2754,8 +2746,4 @@ export class TLDrawState extends StateManager<Data> {
onError = () => {
// TODO
}
onBlurEditingShape = () => {
this.completeSession()
}
}

View file

@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/ban-types */
import type { TLBinding, TLRenderInfo } from '@tldraw/core'
import type { TLBinding, TLShapeProps } from '@tldraw/core'
import { TLShape, TLShapeUtil, TLHandle } from '@tldraw/core'
import type { TLPage, TLPageState } from '@tldraw/core'
import type { StoreApi } from 'zustand'
@ -32,7 +32,11 @@ export interface TLDrawMeta {
isDarkMode: boolean
}
export type TLDrawRenderInfo = TLRenderInfo<TLDrawMeta>
export type TLDrawShapeProps<T extends TLDrawShape, E extends Element> = TLShapeProps<
T,
E,
TLDrawMeta
>
export interface Data {
document: TLDrawDocument
@ -171,6 +175,7 @@ export interface ArrowShape extends TLDrawBaseShape {
middle?: Decoration
}
}
export interface EllipseShape extends TLDrawBaseShape {
type: TLDrawShapeType.Ellipse
radius: number[]