kopia lustrzana https://github.com/Tldraw/Tldraw
[fix] Multiplayer bugs on text (#571)
* Update StickyUtil.tsx * Fix sticky text in multiplayer? * fix text and text label * Update TextUtil.tsx * Update TextUtil.tsx * Fix missing empty content button * Create tidy-ducks-visit.md * forcing bump * Update TextUtil.tsx * fix resizing * try again * don't merge editing ids * fixed! * Update utils.ts * downgrade puppeteer * change deps * restore deps * explicit version * keep at it * depspull/573/head
rodzic
aec4d8846c
commit
e8dd64baf7
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
'@tldraw/core': patch
|
||||||
|
'@tldraw/intersect': patch
|
||||||
|
'@tldraw/tldraw': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fix text in multiplayer
|
|
@ -1,4 +1,3 @@
|
||||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { Tldraw, useFileSystem } from '@tldraw/tldraw'
|
import { Tldraw, useFileSystem } from '@tldraw/tldraw'
|
||||||
import { createClient } from '@liveblocks/client'
|
import { createClient } from '@liveblocks/client'
|
||||||
|
|
|
@ -29,13 +29,13 @@
|
||||||
"@tldraw/tldraw": "^1.7.0",
|
"@tldraw/tldraw": "^1.7.0",
|
||||||
"@types/next-auth": "^3.15.0",
|
"@types/next-auth": "^3.15.0",
|
||||||
"aws-sdk": "^2.1053.0",
|
"aws-sdk": "^2.1053.0",
|
||||||
"chrome-aws-lambda": "^8.0.2",
|
|
||||||
"next": "^12.0.7",
|
"next": "^12.0.7",
|
||||||
"next-auth": "^4.0.5",
|
"next-auth": "^4.0.5",
|
||||||
"next-pwa": "^5.4.4",
|
"next-pwa": "^5.4.4",
|
||||||
"next-themes": "^0.0.15",
|
"next-themes": "^0.0.15",
|
||||||
"next-transpile-modules": "^9.0.0",
|
"next-transpile-modules": "^9.0.0",
|
||||||
"puppeteer-core": "^9.0.0",
|
"chrome-aws-lambda": "8.0.2",
|
||||||
|
"puppeteer-core": "9.0.0",
|
||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
"react-dom": "17.0.2"
|
"react-dom": "17.0.2"
|
||||||
},
|
},
|
||||||
|
|
|
@ -46,6 +46,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
defaultViewport: chromium.defaultViewport,
|
defaultViewport: chromium.defaultViewport,
|
||||||
executablePath: await chromium.executablePath,
|
executablePath: await chromium.executablePath,
|
||||||
ignoreHTTPSErrors: true,
|
ignoreHTTPSErrors: true,
|
||||||
|
headless: chromium.headless,
|
||||||
})
|
})
|
||||||
|
|
||||||
const page = await browser.newPage()
|
const page = await browser.newPage()
|
||||||
|
@ -74,9 +75,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
app.selectAll()
|
app.selectAll()
|
||||||
app.zoomToSelection()
|
app.zoomToSelection()
|
||||||
app.selectNone()
|
app.selectNone()
|
||||||
const tlContainer = document.getElementsByClassName('tl-container').item(0) as HTMLElement;
|
const tlContainer = document.getElementsByClassName('tl-container').item(0) as HTMLElement
|
||||||
if (tlContainer) {
|
if (tlContainer) {
|
||||||
tlContainer.style.background = 'transparent';
|
tlContainer.style.background = 'transparent'
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
err = e.message
|
err = e.message
|
||||||
|
@ -87,7 +88,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
}
|
}
|
||||||
const imageBuffer = await page.screenshot({
|
const imageBuffer = await page.screenshot({
|
||||||
type,
|
type,
|
||||||
omitBackground: true
|
omitBackground: true,
|
||||||
})
|
})
|
||||||
await browser.close()
|
await browser.close()
|
||||||
res.status(200).send(imageBuffer)
|
res.status(200).send(imageBuffer)
|
||||||
|
|
|
@ -8,6 +8,7 @@ describe('page', () => {
|
||||||
<Page
|
<Page
|
||||||
page={mockDocument.page}
|
page={mockDocument.page}
|
||||||
pageState={mockDocument.pageState}
|
pageState={mockDocument.pageState}
|
||||||
|
assets={{}}
|
||||||
hideBounds={false}
|
hideBounds={false}
|
||||||
hideIndicators={false}
|
hideIndicators={false}
|
||||||
hideHandles={false}
|
hideHandles={false}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { useTLContext } from './useTLContext'
|
import { useTLContext } from './useTLContext'
|
||||||
|
|
||||||
|
|
|
@ -86,7 +86,7 @@ export function useShapeTree<T extends TLShape, M extends Record<string, unknown
|
||||||
const { callbacks, shapeUtils, bounds } = useTLContext()
|
const { callbacks, shapeUtils, bounds } = useTLContext()
|
||||||
|
|
||||||
const rTimeout = React.useRef<unknown>()
|
const rTimeout = React.useRef<unknown>()
|
||||||
const rPreviousCount = React.useRef(0)
|
const rPreviousCount = React.useRef(-1)
|
||||||
const rShapesIdsToRender = React.useRef(new Set<string>())
|
const rShapesIdsToRender = React.useRef(new Set<string>())
|
||||||
const rShapesToRender = React.useRef(new Set<TLShape>())
|
const rShapesToRender = React.useRef(new Set<TLShape>())
|
||||||
|
|
||||||
|
@ -114,7 +114,9 @@ export function useShapeTree<T extends TLShape, M extends Record<string, unknown
|
||||||
shapesToRender.clear()
|
shapesToRender.clear()
|
||||||
shapesIdsToRender.clear()
|
shapesIdsToRender.clear()
|
||||||
|
|
||||||
Object.values(page.shapes)
|
const allShapes = Object.values(page.shapes)
|
||||||
|
|
||||||
|
allShapes
|
||||||
.filter(
|
.filter(
|
||||||
(shape) =>
|
(shape) =>
|
||||||
// Always render shapes that are flagged as stateful
|
// Always render shapes that are flagged as stateful
|
||||||
|
@ -139,7 +141,7 @@ export function useShapeTree<T extends TLShape, M extends Record<string, unknown
|
||||||
shapesToRender.add(page.shapes[shape.parentId])
|
shapesToRender.add(page.shapes[shape.parentId])
|
||||||
})
|
})
|
||||||
|
|
||||||
// Call onChange callback when number of rendering shapes changes
|
// Call onRenderCountChange callback when number of rendering shapes changes
|
||||||
|
|
||||||
if (shapesToRender.size !== rPreviousCount.current) {
|
if (shapesToRender.size !== rPreviousCount.current) {
|
||||||
// Use a timeout to clear call stack, in case the onChange handler
|
// Use a timeout to clear call stack, in case the onChange handler
|
||||||
|
|
|
@ -93,11 +93,11 @@ export class Utils {
|
||||||
* Recursively clone an object or array.
|
* Recursively clone an object or array.
|
||||||
* @param obj
|
* @param obj
|
||||||
*/
|
*/
|
||||||
static deepClone<T extends unknown>(obj: T): T {
|
static deepClone<T>(obj: T): T {
|
||||||
if (obj === null) return obj
|
if (obj === null) return obj
|
||||||
|
|
||||||
if (Array.isArray(obj)) {
|
if (Array.isArray(obj)) {
|
||||||
return [...obj] as T
|
return [...obj] as unknown as T
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof obj === 'object') {
|
if (typeof obj === 'object') {
|
||||||
|
@ -111,7 +111,7 @@ export class Utils {
|
||||||
: obj[key as keyof T])
|
: obj[key as keyof T])
|
||||||
)
|
)
|
||||||
|
|
||||||
return clone as T
|
return clone as unknown as T
|
||||||
}
|
}
|
||||||
|
|
||||||
return obj
|
return obj
|
||||||
|
|
|
@ -39,9 +39,5 @@
|
||||||
"@tldraw/vec": "*",
|
"@tldraw/vec": "*",
|
||||||
"lask": "^0.0.29"
|
"lask": "^0.0.29"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
|
||||||
"@tldraw/vec": "*",
|
|
||||||
"lask": "^0.0.29"
|
|
||||||
},
|
|
||||||
"gitHead": "4b1137849ad07da36fc8f0f19cb64e7535a79296"
|
"gitHead": "4b1137849ad07da36fc8f0f19cb64e7535a79296"
|
||||||
}
|
}
|
|
@ -150,9 +150,8 @@ export function Tldraw({
|
||||||
})
|
})
|
||||||
|
|
||||||
// Create a new app if the `id` prop changes.
|
// Create a new app if the `id` prop changes.
|
||||||
React.useEffect(() => {
|
React.useLayoutEffect(() => {
|
||||||
if (id === sId) return
|
if (id === sId) return
|
||||||
|
|
||||||
const newApp = new TldrawApp(id, {
|
const newApp = new TldrawApp(id, {
|
||||||
onMount,
|
onMount,
|
||||||
onChange,
|
onChange,
|
||||||
|
@ -175,6 +174,7 @@ export function Tldraw({
|
||||||
onExport,
|
onExport,
|
||||||
})
|
})
|
||||||
setSId(id)
|
setSId(id)
|
||||||
|
|
||||||
setApp(newApp)
|
setApp(newApp)
|
||||||
}, [sId, id])
|
}, [sId, id])
|
||||||
|
|
||||||
|
|
|
@ -5,9 +5,12 @@ import { useTldrawApp } from '~hooks'
|
||||||
import { RowButton } from '~components/Primitives/RowButton'
|
import { RowButton } from '~components/Primitives/RowButton'
|
||||||
import { MenuContent } from '~components/Primitives/MenuContent'
|
import { MenuContent } from '~components/Primitives/MenuContent'
|
||||||
|
|
||||||
const isEmptyCanvasSelector = (s: TDSnapshot) =>
|
const isEmptyCanvasSelector = (s: TDSnapshot) => {
|
||||||
Object.keys(s.document.pages[s.appState.currentPageId].shapes).length > 0 &&
|
return (
|
||||||
s.appState.isEmptyCanvas
|
s.appState.isEmptyCanvas &&
|
||||||
|
Object.keys(s.document.pages[s.appState.currentPageId].shapes).length > 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export const BackToContent = React.memo(function BackToContent() {
|
export const BackToContent = React.memo(function BackToContent() {
|
||||||
const app = useTldrawApp()
|
const app = useTldrawApp()
|
||||||
|
|
|
@ -85,5 +85,7 @@ export const USER_COLORS = [
|
||||||
|
|
||||||
export const isSafari =
|
export const isSafari =
|
||||||
typeof Window === 'undefined' ? false : /^((?!chrome|android).)*safari/i.test(navigator.userAgent)
|
typeof Window === 'undefined' ? false : /^((?!chrome|android).)*safari/i.test(navigator.userAgent)
|
||||||
|
|
||||||
export const IMAGE_EXTENSIONS = ['.png', '.svg', '.jpg', '.jpeg', '.gif']
|
export const IMAGE_EXTENSIONS = ['.png', '.svg', '.jpg', '.jpeg', '.gif']
|
||||||
|
|
||||||
export const VIDEO_EXTENSIONS = isSafari ? [] : ['.mp4', '.webm']
|
export const VIDEO_EXTENSIONS = isSafari ? [] : ['.mp4', '.webm']
|
||||||
|
|
|
@ -95,8 +95,13 @@ export class StateManager<T extends Record<string, any>> {
|
||||||
|
|
||||||
await idb.set(id + '_version', version || -1)
|
await idb.set(id + '_version', version || -1)
|
||||||
|
|
||||||
|
// why is this necessary? but it is...
|
||||||
|
const prevEmpty = this._state.appState.isEmptyCanvas
|
||||||
|
|
||||||
this._state = deepCopy(next)
|
this._state = deepCopy(next)
|
||||||
this._snapshot = deepCopy(next)
|
this._snapshot = deepCopy(next)
|
||||||
|
|
||||||
|
this._state.appState.isEmptyCanvas = prevEmpty
|
||||||
this.store.setState(this._state, true)
|
this.store.setState(this._state, true)
|
||||||
} else {
|
} else {
|
||||||
await idb.set(id + '_version', version || -1)
|
await idb.set(id + '_version', version || -1)
|
||||||
|
|
|
@ -611,7 +611,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getReservedContent = (ids: string[], pageId = this.currentPageId) => {
|
getReservedContent = (coreReservedIds: string[], pageId = this.currentPageId) => {
|
||||||
const { bindings } = this.document.pages[pageId]
|
const { bindings } = this.document.pages[pageId]
|
||||||
|
|
||||||
// We want to know which shapes we need to
|
// We want to know which shapes we need to
|
||||||
|
@ -627,7 +627,8 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||||
// Unique set of shape ids that are going to be reserved
|
// Unique set of shape ids that are going to be reserved
|
||||||
const reservedShapeIds: string[] = []
|
const reservedShapeIds: string[] = []
|
||||||
|
|
||||||
if (this.session) ids.forEach((id) => reservedShapeIds.push(id))
|
if (this.session) coreReservedIds.forEach((id) => reservedShapeIds.push(id))
|
||||||
|
if (this.pageState.editingId) reservedShapeIds.push(this.pageState.editingId)
|
||||||
|
|
||||||
const strongReservedShapeIds = new Set(reservedShapeIds)
|
const strongReservedShapeIds = new Set(reservedShapeIds)
|
||||||
|
|
||||||
|
@ -686,7 +687,8 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||||
|
|
||||||
const coreReservedIds = [...selectedIds]
|
const coreReservedIds = [...selectedIds]
|
||||||
|
|
||||||
if (editingId) coreReservedIds.push(editingId)
|
const editingShape = editingId && current.document.pages[this.currentPageId].shapes[editingId]
|
||||||
|
if (editingShape) coreReservedIds.push(editingShape.id)
|
||||||
|
|
||||||
const { reservedShapes, reservedBindings, strongReservedShapeIds } = this.getReservedContent(
|
const { reservedShapes, reservedBindings, strongReservedShapeIds } = this.getReservedContent(
|
||||||
coreReservedIds,
|
coreReservedIds,
|
||||||
|
@ -712,7 +714,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||||
strongReservedShapeIds.has(reservedShape.id)
|
strongReservedShapeIds.has(reservedShape.id)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
reservedShapes[reservedShape.id] = incomingShape
|
shapes[reservedShape.id] = incomingShape
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -720,7 +722,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||||
|
|
||||||
// Allow decorations (of an arrow) to be changed
|
// Allow decorations (of an arrow) to be changed
|
||||||
if ('decorations' in incomingShape && 'decorations' in reservedShape) {
|
if ('decorations' in incomingShape && 'decorations' in reservedShape) {
|
||||||
reservedShape.decorations = incomingShape.decorations
|
shapes[reservedShape.id] = { ...reservedShape, decorations: incomingShape.decorations }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow the shape's style to be changed
|
// Allow the shape's style to be changed
|
||||||
|
@ -740,6 +742,10 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||||
...reservedShapes,
|
...reservedShapes,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (editingShape) {
|
||||||
|
nextShapes[editingShape.id] = editingShape
|
||||||
|
}
|
||||||
|
|
||||||
const nextBindings = {
|
const nextBindings = {
|
||||||
...bindings,
|
...bindings,
|
||||||
...reservedBindings,
|
...reservedBindings,
|
||||||
|
|
|
@ -59,18 +59,21 @@ export class StickyUtil extends TDShapeUtil<T, E> {
|
||||||
|
|
||||||
const rText = React.useRef<HTMLDivElement>(null)
|
const rText = React.useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
const rTextContent = React.useRef(shape.text)
|
||||||
|
|
||||||
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 handleLabelChange = React.useCallback(
|
const handleTextChange = React.useCallback(
|
||||||
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
|
rTextContent.current = TLDR.normalizeText(e.currentTarget.value)
|
||||||
onShapeChange?.({
|
onShapeChange?.({
|
||||||
id: shape.id,
|
id: shape.id,
|
||||||
type: shape.type,
|
type: shape.type,
|
||||||
text: TLDR.normalizeText(e.currentTarget.value),
|
text: rTextContent.current,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
[onShapeChange]
|
[onShapeChange]
|
||||||
|
@ -115,7 +118,8 @@ export class StickyUtil extends TDShapeUtil<T, E> {
|
||||||
TextAreaUtils.indent(e.currentTarget)
|
TextAreaUtils.indent(e.currentTarget)
|
||||||
}
|
}
|
||||||
|
|
||||||
onShapeChange?.({ ...shape, text: TLDR.normalizeText(e.currentTarget.value) })
|
rTextContent.current = TLDR.normalizeText(e.currentTarget.value)
|
||||||
|
onShapeChange?.({ ...shape, text: rTextContent.current })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[shape, onShapeChange]
|
[shape, onShapeChange]
|
||||||
|
@ -138,6 +142,7 @@ export class StickyUtil extends TDShapeUtil<T, E> {
|
||||||
// Focus when editing changes to true
|
// Focus when editing changes to true
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (isEditing) {
|
if (isEditing) {
|
||||||
|
rTextContent.current = shape.text
|
||||||
rIsMounted.current = true
|
rIsMounted.current = true
|
||||||
const elm = rTextArea.current!
|
const elm = rTextArea.current!
|
||||||
elm.focus()
|
elm.focus()
|
||||||
|
@ -205,14 +210,14 @@ export class StickyUtil extends TDShapeUtil<T, E> {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<StyledText ref={rText} isEditing={isEditing} alignment={shape.style.textAlign}>
|
<StyledText ref={rText} isEditing={isEditing} alignment={shape.style.textAlign}>
|
||||||
{shape.text}​
|
{isEditing ? rTextContent.current : shape.text}​
|
||||||
</StyledText>
|
</StyledText>
|
||||||
{isEditing && (
|
{isEditing && (
|
||||||
<StyledTextArea
|
<StyledTextArea
|
||||||
ref={rTextArea}
|
ref={rTextArea}
|
||||||
onPointerDown={handlePointerDown}
|
onPointerDown={handlePointerDown}
|
||||||
value={shape.text}
|
value={isEditing ? rTextContent.current : shape.text}
|
||||||
onChange={handleLabelChange}
|
onChange={handleTextChange}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
onFocus={handleFocus}
|
onFocus={handleFocus}
|
||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
|
|
|
@ -46,50 +46,44 @@ export class TextUtil extends TDShapeUtil<T, E> {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
texts = new Map<string, string>()
|
||||||
|
|
||||||
Component = TDShapeUtil.Component<T, E, TDMeta>(
|
Component = TDShapeUtil.Component<T, E, TDMeta>(
|
||||||
({ shape, isBinding, isGhost, isEditing, onShapeBlur, onShapeChange, meta, events }, ref) => {
|
({ shape, isBinding, isGhost, isEditing, onShapeBlur, onShapeChange, meta, events }, ref) => {
|
||||||
const { text, style } = shape
|
const { text, style } = shape
|
||||||
const styles = getShapeStyle(style, meta.isDarkMode)
|
const styles = getShapeStyle(style, meta.isDarkMode)
|
||||||
const font = getFontStyle(shape.style)
|
const font = getFontStyle(shape.style)
|
||||||
|
|
||||||
const rInput = React.useRef<HTMLTextAreaElement>(null)
|
const rInput = React.useRef<HTMLTextAreaElement>(null)
|
||||||
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>) => {
|
||||||
let delta = [0, 0]
|
let delta = [0, 0]
|
||||||
|
const newText = TLDR.normalizeText(e.currentTarget.value)
|
||||||
const currentBounds = this.getBounds(shape)
|
const currentBounds = this.getBounds(shape)
|
||||||
|
this.texts.set(shape.id, newText)
|
||||||
|
const nextBounds = this.getBounds({
|
||||||
|
...shape,
|
||||||
|
text: newText,
|
||||||
|
})
|
||||||
switch (shape.style.textAlign) {
|
switch (shape.style.textAlign) {
|
||||||
case AlignStyle.Start: {
|
case AlignStyle.Start: {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case AlignStyle.Middle: {
|
case AlignStyle.Middle: {
|
||||||
const nextBounds = this.getBounds({
|
|
||||||
...shape,
|
|
||||||
text: TLDR.normalizeText(e.currentTarget.value),
|
|
||||||
})
|
|
||||||
|
|
||||||
delta = Vec.div([nextBounds.width - currentBounds.width, 0], 2)
|
delta = Vec.div([nextBounds.width - currentBounds.width, 0], 2)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case AlignStyle.End: {
|
case AlignStyle.End: {
|
||||||
const nextBounds = this.getBounds({
|
|
||||||
...shape,
|
|
||||||
text: TLDR.normalizeText(e.currentTarget.value),
|
|
||||||
})
|
|
||||||
|
|
||||||
delta = [nextBounds.width - currentBounds.width, 0]
|
delta = [nextBounds.width - currentBounds.width, 0]
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onShapeChange?.({
|
onShapeChange?.({
|
||||||
...shape,
|
...shape,
|
||||||
id: shape.id,
|
id: shape.id,
|
||||||
point: Vec.sub(shape.point, delta),
|
point: Vec.sub(shape.point, delta),
|
||||||
text: TLDR.normalizeText(e.currentTarget.value),
|
text: newText,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
[shape.id, shape.point]
|
[shape.id, shape.point]
|
||||||
|
@ -97,7 +91,8 @@ export class TextUtil extends TDShapeUtil<T, E> {
|
||||||
|
|
||||||
const onChange = React.useCallback(
|
const onChange = React.useCallback(
|
||||||
(text: string) => {
|
(text: string) => {
|
||||||
onShapeChange?.({ id: shape.id, text })
|
this.texts.set(shape.id, TLDR.normalizeText(text))
|
||||||
|
onShapeChange?.({ id: shape.id, text: this.texts.get(shape.id)! })
|
||||||
},
|
},
|
||||||
[shape.id]
|
[shape.id]
|
||||||
)
|
)
|
||||||
|
@ -113,7 +108,6 @@ export class TextUtil extends TDShapeUtil<T, E> {
|
||||||
(e: React.FocusEvent<HTMLTextAreaElement>) => {
|
(e: React.FocusEvent<HTMLTextAreaElement>) => {
|
||||||
if (!isEditing) return
|
if (!isEditing) return
|
||||||
if (!rIsMounted.current) return
|
if (!rIsMounted.current) return
|
||||||
|
|
||||||
if (document.activeElement === e.currentTarget) {
|
if (document.activeElement === e.currentTarget) {
|
||||||
e.currentTarget.select()
|
e.currentTarget.select()
|
||||||
}
|
}
|
||||||
|
@ -132,6 +126,7 @@ export class TextUtil extends TDShapeUtil<T, E> {
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (isEditing) {
|
if (isEditing) {
|
||||||
|
this.texts.set(shape.id, text)
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
rIsMounted.current = true
|
rIsMounted.current = true
|
||||||
const elm = rInput.current
|
const elm = rInput.current
|
||||||
|
@ -219,7 +214,7 @@ export class TextUtil extends TDShapeUtil<T, E> {
|
||||||
return { minX: 0, minY: 0, maxX: 10, maxY: 10, width: 10, height: 10 }
|
return { minX: 0, minY: 0, maxX: 10, maxY: 10, width: 10, height: 10 }
|
||||||
}
|
}
|
||||||
|
|
||||||
melm.textContent = shape.text
|
melm.textContent = this.texts.get(shape.id) ?? shape.text
|
||||||
melm.style.font = getFontStyle(shape.style)
|
melm.style.font = getFontStyle(shape.style)
|
||||||
|
|
||||||
// In tests, offsetWidth and offsetHeight will be 0
|
// In tests, offsetWidth and offsetHeight will be 0
|
||||||
|
|
|
@ -33,9 +33,12 @@ export const TextLabel = React.memo(function TextLabel({
|
||||||
const rIsMounted = React.useRef(false)
|
const rIsMounted = React.useRef(false)
|
||||||
const size = getTextLabelSize(text, font)
|
const size = getTextLabelSize(text, font)
|
||||||
|
|
||||||
|
const rTextContent = React.useRef(text)
|
||||||
|
|
||||||
const handleChange = React.useCallback(
|
const handleChange = React.useCallback(
|
||||||
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
onChange(TLDR.normalizeText(e.currentTarget.value))
|
rTextContent.current = TLDR.normalizeText(e.currentTarget.value)
|
||||||
|
onChange(rTextContent.current)
|
||||||
},
|
},
|
||||||
[onChange]
|
[onChange]
|
||||||
)
|
)
|
||||||
|
@ -73,6 +76,7 @@ export const TextLabel = React.memo(function TextLabel({
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (isEditing) {
|
if (isEditing) {
|
||||||
|
rTextContent.current = text
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
rIsMounted.current = true
|
rIsMounted.current = true
|
||||||
const elm = rInput.current
|
const elm = rInput.current
|
||||||
|
@ -126,7 +130,7 @@ export const TextLabel = React.memo(function TextLabel({
|
||||||
wrap="off"
|
wrap="off"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
datatype="wysiwyg"
|
datatype="wysiwyg"
|
||||||
defaultValue={text}
|
defaultValue={rTextContent.current}
|
||||||
color={color}
|
color={color}
|
||||||
onFocus={handleFocus}
|
onFocus={handleFocus}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
|
|
|
@ -20,7 +20,6 @@ import type {
|
||||||
TLShapeBlurHandler,
|
TLShapeBlurHandler,
|
||||||
TLShapeCloneHandler,
|
TLShapeCloneHandler,
|
||||||
TLAsset,
|
TLAsset,
|
||||||
TLBounds,
|
|
||||||
} from '@tldraw/core'
|
} from '@tldraw/core'
|
||||||
|
|
||||||
/* -------------------------------------------------- */
|
/* -------------------------------------------------- */
|
||||||
|
|
|
@ -76,7 +76,7 @@
|
||||||
},
|
},
|
||||||
"test": {
|
"test": {
|
||||||
"dependsOn": [
|
"dependsOn": [
|
||||||
"build"
|
"build:packages"
|
||||||
],
|
],
|
||||||
"outputs": []
|
"outputs": []
|
||||||
},
|
},
|
||||||
|
|
Ładowanie…
Reference in New Issue