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 { Tldraw, useFileSystem } from '@tldraw/tldraw'
|
||||
import { createClient } from '@liveblocks/client'
|
||||
|
|
|
@ -29,13 +29,13 @@
|
|||
"@tldraw/tldraw": "^1.7.0",
|
||||
"@types/next-auth": "^3.15.0",
|
||||
"aws-sdk": "^2.1053.0",
|
||||
"chrome-aws-lambda": "^8.0.2",
|
||||
"next": "^12.0.7",
|
||||
"next-auth": "^4.0.5",
|
||||
"next-pwa": "^5.4.4",
|
||||
"next-themes": "^0.0.15",
|
||||
"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-dom": "17.0.2"
|
||||
},
|
||||
|
|
|
@ -46,6 +46,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
defaultViewport: chromium.defaultViewport,
|
||||
executablePath: await chromium.executablePath,
|
||||
ignoreHTTPSErrors: true,
|
||||
headless: chromium.headless,
|
||||
})
|
||||
|
||||
const page = await browser.newPage()
|
||||
|
@ -74,9 +75,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
app.selectAll()
|
||||
app.zoomToSelection()
|
||||
app.selectNone()
|
||||
const tlContainer = document.getElementsByClassName('tl-container').item(0) as HTMLElement;
|
||||
const tlContainer = document.getElementsByClassName('tl-container').item(0) as HTMLElement
|
||||
if (tlContainer) {
|
||||
tlContainer.style.background = 'transparent';
|
||||
tlContainer.style.background = 'transparent'
|
||||
}
|
||||
} catch (e) {
|
||||
err = e.message
|
||||
|
@ -87,7 +88,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
}
|
||||
const imageBuffer = await page.screenshot({
|
||||
type,
|
||||
omitBackground: true
|
||||
omitBackground: true,
|
||||
})
|
||||
await browser.close()
|
||||
res.status(200).send(imageBuffer)
|
||||
|
|
|
@ -8,6 +8,7 @@ describe('page', () => {
|
|||
<Page
|
||||
page={mockDocument.page}
|
||||
pageState={mockDocument.pageState}
|
||||
assets={{}}
|
||||
hideBounds={false}
|
||||
hideIndicators={false}
|
||||
hideHandles={false}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import * as React from 'react'
|
||||
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 rTimeout = React.useRef<unknown>()
|
||||
const rPreviousCount = React.useRef(0)
|
||||
const rPreviousCount = React.useRef(-1)
|
||||
const rShapesIdsToRender = React.useRef(new Set<string>())
|
||||
const rShapesToRender = React.useRef(new Set<TLShape>())
|
||||
|
||||
|
@ -114,7 +114,9 @@ export function useShapeTree<T extends TLShape, M extends Record<string, unknown
|
|||
shapesToRender.clear()
|
||||
shapesIdsToRender.clear()
|
||||
|
||||
Object.values(page.shapes)
|
||||
const allShapes = Object.values(page.shapes)
|
||||
|
||||
allShapes
|
||||
.filter(
|
||||
(shape) =>
|
||||
// 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])
|
||||
})
|
||||
|
||||
// Call onChange callback when number of rendering shapes changes
|
||||
// Call onRenderCountChange callback when number of rendering shapes changes
|
||||
|
||||
if (shapesToRender.size !== rPreviousCount.current) {
|
||||
// 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.
|
||||
* @param obj
|
||||
*/
|
||||
static deepClone<T extends unknown>(obj: T): T {
|
||||
static deepClone<T>(obj: T): T {
|
||||
if (obj === null) return obj
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
return [...obj] as T
|
||||
return [...obj] as unknown as T
|
||||
}
|
||||
|
||||
if (typeof obj === 'object') {
|
||||
|
@ -111,7 +111,7 @@ export class Utils {
|
|||
: obj[key as keyof T])
|
||||
)
|
||||
|
||||
return clone as T
|
||||
return clone as unknown as T
|
||||
}
|
||||
|
||||
return obj
|
||||
|
|
|
@ -39,9 +39,5 @@
|
|||
"@tldraw/vec": "*",
|
||||
"lask": "^0.0.29"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tldraw/vec": "*",
|
||||
"lask": "^0.0.29"
|
||||
},
|
||||
"gitHead": "4b1137849ad07da36fc8f0f19cb64e7535a79296"
|
||||
}
|
|
@ -150,9 +150,8 @@ export function Tldraw({
|
|||
})
|
||||
|
||||
// Create a new app if the `id` prop changes.
|
||||
React.useEffect(() => {
|
||||
React.useLayoutEffect(() => {
|
||||
if (id === sId) return
|
||||
|
||||
const newApp = new TldrawApp(id, {
|
||||
onMount,
|
||||
onChange,
|
||||
|
@ -175,6 +174,7 @@ export function Tldraw({
|
|||
onExport,
|
||||
})
|
||||
setSId(id)
|
||||
|
||||
setApp(newApp)
|
||||
}, [sId, id])
|
||||
|
||||
|
|
|
@ -5,9 +5,12 @@ import { useTldrawApp } from '~hooks'
|
|||
import { RowButton } from '~components/Primitives/RowButton'
|
||||
import { MenuContent } from '~components/Primitives/MenuContent'
|
||||
|
||||
const isEmptyCanvasSelector = (s: TDSnapshot) =>
|
||||
Object.keys(s.document.pages[s.appState.currentPageId].shapes).length > 0 &&
|
||||
s.appState.isEmptyCanvas
|
||||
const isEmptyCanvasSelector = (s: TDSnapshot) => {
|
||||
return (
|
||||
s.appState.isEmptyCanvas &&
|
||||
Object.keys(s.document.pages[s.appState.currentPageId].shapes).length > 0
|
||||
)
|
||||
}
|
||||
|
||||
export const BackToContent = React.memo(function BackToContent() {
|
||||
const app = useTldrawApp()
|
||||
|
|
|
@ -85,5 +85,7 @@ export const USER_COLORS = [
|
|||
|
||||
export const isSafari =
|
||||
typeof Window === 'undefined' ? false : /^((?!chrome|android).)*safari/i.test(navigator.userAgent)
|
||||
|
||||
export const IMAGE_EXTENSIONS = ['.png', '.svg', '.jpg', '.jpeg', '.gif']
|
||||
|
||||
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)
|
||||
|
||||
// why is this necessary? but it is...
|
||||
const prevEmpty = this._state.appState.isEmptyCanvas
|
||||
|
||||
this._state = deepCopy(next)
|
||||
this._snapshot = deepCopy(next)
|
||||
|
||||
this._state.appState.isEmptyCanvas = prevEmpty
|
||||
this.store.setState(this._state, true)
|
||||
} else {
|
||||
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]
|
||||
|
||||
// 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
|
||||
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)
|
||||
|
||||
|
@ -686,7 +687,8 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
|
||||
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(
|
||||
coreReservedIds,
|
||||
|
@ -712,7 +714,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
strongReservedShapeIds.has(reservedShape.id)
|
||||
)
|
||||
) {
|
||||
reservedShapes[reservedShape.id] = incomingShape
|
||||
shapes[reservedShape.id] = incomingShape
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -720,7 +722,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
|
||||
// Allow decorations (of an arrow) to be changed
|
||||
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
|
||||
|
@ -740,6 +742,10 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
...reservedShapes,
|
||||
}
|
||||
|
||||
if (editingShape) {
|
||||
nextShapes[editingShape.id] = editingShape
|
||||
}
|
||||
|
||||
const nextBindings = {
|
||||
...bindings,
|
||||
...reservedBindings,
|
||||
|
|
|
@ -59,18 +59,21 @@ export class StickyUtil extends TDShapeUtil<T, E> {
|
|||
|
||||
const rText = React.useRef<HTMLDivElement>(null)
|
||||
|
||||
const rTextContent = React.useRef(shape.text)
|
||||
|
||||
const rIsMounted = React.useRef(false)
|
||||
|
||||
const handlePointerDown = React.useCallback((e: React.PointerEvent) => {
|
||||
e.stopPropagation()
|
||||
}, [])
|
||||
|
||||
const handleLabelChange = React.useCallback(
|
||||
const handleTextChange = React.useCallback(
|
||||
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
rTextContent.current = TLDR.normalizeText(e.currentTarget.value)
|
||||
onShapeChange?.({
|
||||
id: shape.id,
|
||||
type: shape.type,
|
||||
text: TLDR.normalizeText(e.currentTarget.value),
|
||||
text: rTextContent.current,
|
||||
})
|
||||
},
|
||||
[onShapeChange]
|
||||
|
@ -115,7 +118,8 @@ export class StickyUtil extends TDShapeUtil<T, E> {
|
|||
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]
|
||||
|
@ -138,6 +142,7 @@ export class StickyUtil extends TDShapeUtil<T, E> {
|
|||
// Focus when editing changes to true
|
||||
React.useEffect(() => {
|
||||
if (isEditing) {
|
||||
rTextContent.current = shape.text
|
||||
rIsMounted.current = true
|
||||
const elm = rTextArea.current!
|
||||
elm.focus()
|
||||
|
@ -205,14 +210,14 @@ export class StickyUtil extends TDShapeUtil<T, E> {
|
|||
/>
|
||||
)}
|
||||
<StyledText ref={rText} isEditing={isEditing} alignment={shape.style.textAlign}>
|
||||
{shape.text}​
|
||||
{isEditing ? rTextContent.current : shape.text}​
|
||||
</StyledText>
|
||||
{isEditing && (
|
||||
<StyledTextArea
|
||||
ref={rTextArea}
|
||||
onPointerDown={handlePointerDown}
|
||||
value={shape.text}
|
||||
onChange={handleLabelChange}
|
||||
value={isEditing ? rTextContent.current : shape.text}
|
||||
onChange={handleTextChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
|
|
|
@ -46,50 +46,44 @@ export class TextUtil extends TDShapeUtil<T, E> {
|
|||
)
|
||||
}
|
||||
|
||||
texts = new Map<string, string>()
|
||||
|
||||
Component = TDShapeUtil.Component<T, E, TDMeta>(
|
||||
({ shape, isBinding, isGhost, isEditing, onShapeBlur, onShapeChange, meta, events }, ref) => {
|
||||
const { text, style } = shape
|
||||
const styles = getShapeStyle(style, meta.isDarkMode)
|
||||
const font = getFontStyle(shape.style)
|
||||
|
||||
const rInput = React.useRef<HTMLTextAreaElement>(null)
|
||||
const rIsMounted = React.useRef(false)
|
||||
|
||||
const handleChange = React.useCallback(
|
||||
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
let delta = [0, 0]
|
||||
|
||||
const newText = TLDR.normalizeText(e.currentTarget.value)
|
||||
const currentBounds = this.getBounds(shape)
|
||||
|
||||
this.texts.set(shape.id, newText)
|
||||
const nextBounds = this.getBounds({
|
||||
...shape,
|
||||
text: newText,
|
||||
})
|
||||
switch (shape.style.textAlign) {
|
||||
case AlignStyle.Start: {
|
||||
break
|
||||
}
|
||||
case AlignStyle.Middle: {
|
||||
const nextBounds = this.getBounds({
|
||||
...shape,
|
||||
text: TLDR.normalizeText(e.currentTarget.value),
|
||||
})
|
||||
|
||||
delta = Vec.div([nextBounds.width - currentBounds.width, 0], 2)
|
||||
break
|
||||
}
|
||||
case AlignStyle.End: {
|
||||
const nextBounds = this.getBounds({
|
||||
...shape,
|
||||
text: TLDR.normalizeText(e.currentTarget.value),
|
||||
})
|
||||
|
||||
delta = [nextBounds.width - currentBounds.width, 0]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
onShapeChange?.({
|
||||
...shape,
|
||||
id: shape.id,
|
||||
point: Vec.sub(shape.point, delta),
|
||||
text: TLDR.normalizeText(e.currentTarget.value),
|
||||
text: newText,
|
||||
})
|
||||
},
|
||||
[shape.id, shape.point]
|
||||
|
@ -97,7 +91,8 @@ export class TextUtil extends TDShapeUtil<T, E> {
|
|||
|
||||
const onChange = React.useCallback(
|
||||
(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]
|
||||
)
|
||||
|
@ -113,7 +108,6 @@ export class TextUtil extends TDShapeUtil<T, E> {
|
|||
(e: React.FocusEvent<HTMLTextAreaElement>) => {
|
||||
if (!isEditing) return
|
||||
if (!rIsMounted.current) return
|
||||
|
||||
if (document.activeElement === e.currentTarget) {
|
||||
e.currentTarget.select()
|
||||
}
|
||||
|
@ -132,6 +126,7 @@ export class TextUtil extends TDShapeUtil<T, E> {
|
|||
|
||||
React.useEffect(() => {
|
||||
if (isEditing) {
|
||||
this.texts.set(shape.id, text)
|
||||
requestAnimationFrame(() => {
|
||||
rIsMounted.current = true
|
||||
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 }
|
||||
}
|
||||
|
||||
melm.textContent = shape.text
|
||||
melm.textContent = this.texts.get(shape.id) ?? shape.text
|
||||
melm.style.font = getFontStyle(shape.style)
|
||||
|
||||
// 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 size = getTextLabelSize(text, font)
|
||||
|
||||
const rTextContent = React.useRef(text)
|
||||
|
||||
const handleChange = React.useCallback(
|
||||
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
onChange(TLDR.normalizeText(e.currentTarget.value))
|
||||
rTextContent.current = TLDR.normalizeText(e.currentTarget.value)
|
||||
onChange(rTextContent.current)
|
||||
},
|
||||
[onChange]
|
||||
)
|
||||
|
@ -73,6 +76,7 @@ export const TextLabel = React.memo(function TextLabel({
|
|||
|
||||
React.useEffect(() => {
|
||||
if (isEditing) {
|
||||
rTextContent.current = text
|
||||
requestAnimationFrame(() => {
|
||||
rIsMounted.current = true
|
||||
const elm = rInput.current
|
||||
|
@ -126,7 +130,7 @@ export const TextLabel = React.memo(function TextLabel({
|
|||
wrap="off"
|
||||
dir="auto"
|
||||
datatype="wysiwyg"
|
||||
defaultValue={text}
|
||||
defaultValue={rTextContent.current}
|
||||
color={color}
|
||||
onFocus={handleFocus}
|
||||
onChange={handleChange}
|
||||
|
|
|
@ -20,7 +20,6 @@ import type {
|
|||
TLShapeBlurHandler,
|
||||
TLShapeCloneHandler,
|
||||
TLAsset,
|
||||
TLBounds,
|
||||
} from '@tldraw/core'
|
||||
|
||||
/* -------------------------------------------------- */
|
||||
|
|
|
@ -76,7 +76,7 @@
|
|||
},
|
||||
"test": {
|
||||
"dependsOn": [
|
||||
"build"
|
||||
"build:packages"
|
||||
],
|
||||
"outputs": []
|
||||
},
|
||||
|
|
Ładowanie…
Reference in New Issue