[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

* deps
pull/573/head
Steve Ruiz 2022-02-11 21:35:24 +00:00 zatwierdzone przez GitHub
rodzic aec4d8846c
commit e8dd64baf7
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
20 zmienionych plików z 895 dodań i 933 usunięć

Wyświetl plik

@ -0,0 +1,7 @@
---
'@tldraw/core': patch
'@tldraw/intersect': patch
'@tldraw/tldraw': patch
---
Fix text in multiplayer

Wyświetl plik

@ -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'

Wyświetl plik

@ -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"
}, },

Wyświetl plik

@ -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)

Wyświetl plik

@ -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}

Wyświetl plik

@ -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'

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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"
} }

Wyświetl plik

@ -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])

Wyświetl plik

@ -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()

Wyświetl plik

@ -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']

Wyświetl plik

@ -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)

Wyświetl plik

@ -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,

Wyświetl plik

@ -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}&#8203; {isEditing ? rTextContent.current : shape.text}&#8203;
</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}

Wyświetl plik

@ -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

Wyświetl plik

@ -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}

Wyświetl plik

@ -20,7 +20,6 @@ import type {
TLShapeBlurHandler, TLShapeBlurHandler,
TLShapeCloneHandler, TLShapeCloneHandler,
TLAsset, TLAsset,
TLBounds,
} from '@tldraw/core' } from '@tldraw/core'
/* -------------------------------------------------- */ /* -------------------------------------------------- */

Wyświetl plik

@ -76,7 +76,7 @@
}, },
"test": { "test": {
"dependsOn": [ "dependsOn": [
"build" "build:packages"
], ],
"outputs": [] "outputs": []
}, },

1694
yarn.lock

Plik diff jest za duży Load Diff