[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 { Tldraw, useFileSystem } from '@tldraw/tldraw'
import { createClient } from '@liveblocks/client'

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -8,6 +8,7 @@ describe('page', () => {
<Page
page={mockDocument.page}
pageState={mockDocument.pageState}
assets={{}}
hideBounds={false}
hideIndicators={false}
hideHandles={false}

Wyświetl plik

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import * as React from 'react'
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 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

Wyświetl plik

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

Wyświetl plik

@ -39,9 +39,5 @@
"@tldraw/vec": "*",
"lask": "^0.0.29"
},
"devDependencies": {
"@tldraw/vec": "*",
"lask": "^0.0.29"
},
"gitHead": "4b1137849ad07da36fc8f0f19cb64e7535a79296"
}

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

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]
// 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,

Wyświetl plik

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

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>(
({ 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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

1694
yarn.lock

Plik diff jest za duży Load Diff