Fix selectedStyles from being new on each update (#293)

This commit is contained in:
Steve Ruiz 2021-11-18 18:18:30 +00:00 committed by GitHub
parent 2f8d973b07
commit e7f61eac34
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 87 additions and 53 deletions

View file

@ -396,7 +396,7 @@ const InnerTldraw = React.memo(function InnerTldraw({
// When the context menu is blurred, close the menu by sending pointer events
// to the context menu's ref. This is a hack around the fact that certain shapes
// stop event propagation, which causes the menu to stay open even when blurred.
const handleContextMenuBlur = React.useCallback<React.FocusEventHandler>((e) => {
const handleMenuBlur = React.useCallback<React.FocusEventHandler>((e) => {
const elm = rWrapper.current
if (!elm) return
if (!elm.contains(e.relatedTarget)) return
@ -405,9 +405,9 @@ const InnerTldraw = React.memo(function InnerTldraw({
}, [])
return (
<StyledLayout ref={rWrapper} tabIndex={0} className={settings.isDarkMode ? dark : ''}>
<StyledLayout ref={rWrapper} tabIndex={-0} className={settings.isDarkMode ? dark : ''}>
<OneOff focusableRef={rWrapper} autofocus={autofocus} />
<ContextMenu onBlur={handleContextMenuBlur}>
<ContextMenu onBlur={handleMenuBlur}>
<Renderer
id={id}
containerRef={rWrapper}
@ -492,7 +492,7 @@ const InnerTldraw = React.memo(function InnerTldraw({
showSponsorLink={showSponsorLink}
/>
<StyledSpacer />
{showTools && !readOnly && <ToolsPanel />}
{showTools && !readOnly && <ToolsPanel onBlur={handleMenuBlur} />}
</>
)}
</StyledUI>

View file

@ -4,6 +4,6 @@ import { renderWithContext } from '~test'
describe('tools panel', () => {
test('mounts component without crashing', () => {
renderWithContext(<ToolsPanel />)
renderWithContext(<ToolsPanel onBlur={() => void null} />)
})
})

View file

@ -10,13 +10,17 @@ import { DeleteButton } from './DeleteButton'
const isDebugModeSelector = (s: TDSnapshot) => s.settings.isDebugMode
export const ToolsPanel = React.memo(function ToolsPanel(): JSX.Element {
interface ToolsPanelProps {
onBlur: React.FocusEventHandler
}
export const ToolsPanel = React.memo(function ToolsPanel({ onBlur }: ToolsPanelProps): JSX.Element {
const app = useTldrawApp()
const isDebugMode = app.useStore(isDebugModeSelector)
return (
<StyledToolsPanelContainer>
<StyledToolsPanelContainer onBlur={onBlur}>
<StyledCenterWrap>
<BackToContent />
<StyledPrimaryTools>

View file

@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/ban-ts-comment */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { StateManager } from 'rko'
import { Patch, StateManager } from 'rko'
import { Vec } from '@tldraw/vec'
import {
TLBoundsEventHandler,
@ -248,15 +248,17 @@ export class TldrawApp extends StateManager<TDSnapshot> {
* @returns The final state
*/
protected cleanup = (state: TDSnapshot, prev: TDSnapshot): TDSnapshot => {
const data = { ...state }
const next = { ...state }
let didChangeStyle = next.appState.selectedStyle !== prev.appState.selectedStyle
// Remove deleted shapes and bindings (in Commands, these will be set to undefined)
if (data.document !== prev.document) {
Object.entries(data.document.pages).forEach(([pageId, page]) => {
if (next.document !== prev.document) {
Object.entries(next.document.pages).forEach(([pageId, page]) => {
if (page === undefined) {
// If page is undefined, delete the page and pagestate
delete data.document.pages[pageId]
delete data.document.pageStates[pageId]
delete next.document.pages[pageId]
delete next.document.pageStates[pageId]
return
}
@ -269,14 +271,20 @@ export class TldrawApp extends StateManager<TDSnapshot> {
const groupsToUpdate = new Set<GroupShape>()
// If shape is undefined, delete the shape
Object.keys(page.shapes).forEach((id) => {
const shape = page.shapes[id]
Object.entries(page.shapes).forEach(([id, shape]) => {
let parentId: string
if (!shape) {
parentId = prevPage.shapes[id]?.parentId
if (!didChangeStyle) {
didChangeStyle = true
}
delete page.shapes[id]
} else {
if (!didChangeStyle && shape.style !== prevPage?.shapes[id]?.style) {
didChangeStyle = true
}
parentId = shape.parentId
}
@ -302,10 +310,10 @@ export class TldrawApp extends StateManager<TDSnapshot> {
.filter((shape) => prevPage?.shapes[shape.id] !== shape)
.map((shape) => shape.id)
data.document.pages[pageId] = page
next.document.pages[pageId] = page
// Get bindings related to the changed shapes
const bindingsToUpdate = TLDR.getRelatedBindings(data, changedShapeIds, pageId)
const bindingsToUpdate = TLDR.getRelatedBindings(next, changedShapeIds, pageId)
// Update all of the bindings we've just collected
bindingsToUpdate.forEach((binding) => {
@ -362,7 +370,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
// Clean up page state, preventing hovers on deleted shapes
const nextPageState: TLPageState = {
...data.document.pageStates[pageId],
...next.document.pageStates[pageId],
}
if (!nextPageState.brush) {
@ -383,16 +391,16 @@ export class TldrawApp extends StateManager<TDSnapshot> {
delete nextPageState.editingId
}
data.document.pageStates[pageId] = nextPageState
next.document.pageStates[pageId] = nextPageState
})
}
const currentPageId = data.appState.currentPageId
const currentPageId = next.appState.currentPageId
const currentPageState = data.document.pageStates[currentPageId]
const currentPageState = next.document.pageStates[currentPageId]
if (data.room && data.room !== prev.room) {
const room = { ...data.room, users: { ...data.room.users } }
if (next.room && next.room !== prev.room) {
const room = { ...next.room, users: { ...next.room.users } }
// Remove any exited users
if (prev.room) {
@ -405,12 +413,12 @@ export class TldrawApp extends StateManager<TDSnapshot> {
})
}
data.room = room
next.room = room
}
if (data.room) {
data.room.users[data.room.userId] = {
...data.room.users[data.room.userId],
if (next.room) {
next.room.users[next.room.userId] = {
...next.room.users[next.room.userId],
point: this.currentPoint,
selectedIds: currentPageState.selectedIds,
}
@ -418,12 +426,14 @@ export class TldrawApp extends StateManager<TDSnapshot> {
// Apply selected style change, if any
const newSelectedStyle = TLDR.getSelectedStyle(data, currentPageId)
if (didChangeStyle) {
const newSelectedStyle = TLDR.getSelectedStyle(next, currentPageId)
if (newSelectedStyle) {
data.appState = {
...data.appState,
selectedStyle: newSelectedStyle,
if (newSelectedStyle) {
next.appState = {
...next.appState,
selectedStyle: newSelectedStyle,
}
}
}
@ -431,10 +441,10 @@ export class TldrawApp extends StateManager<TDSnapshot> {
// This is a broad solution but not a very good one: the UX
// for interacting with a readOnly document will be more nuanced.
if (this.readOnly) {
data.document.pages = prev.document.pages
next.document.pages = prev.document.pages
}
return data
return next
}
onPatch = (state: TDSnapshot, id?: string) => {
@ -2168,6 +2178,18 @@ export class TldrawApp extends StateManager<TDSnapshot> {
* @param ids The ids of the shapes to change (defaults to selection).
*/
style = (style: Partial<ShapeStyles>, ids = this.selectedIds): this => {
if (ids.length === 0) {
this.patchState(
{
appState: {
selectedStyle: style,
},
},
'changed_style'
)
return this
}
return this.setState(Commands.styleShapes(this, ids, style))
}

View file

@ -310,23 +310,34 @@ export class TranslateSession extends BaseSession {
// Set the selected ids to the clones
nextPageState.selectedIds = clones.map((clone) => clone.id)
// Either way, move the clones
clones.forEach((clone) => {
const current = (nextShapes[clone.id] || this.app.getShape(clone.id)) as TDShape
if (!current.point) throw Error('No point on that clone!')
nextShapes[clone.id] = {
...clone,
point: Vec.round(Vec.add(current.point, movement)),
}
})
} else {
if (this.cloneInfo.state === 'empty') throw Error
const { clones } = this.cloneInfo
// Either way, move the clones
clones.forEach((clone) => {
const current = (nextShapes[clone.id] || this.app.getShape(clone.id)) as TDShape
if (!current.point) throw Error('No point on that clone!')
nextShapes[clone.id] = {
point: Vec.round(Vec.add(current.point, movement)),
}
})
}
if (this.cloneInfo.state === 'empty') throw Error
const { clones } = this.cloneInfo
// Either way, move the clones
clones.forEach((clone) => {
const current = (nextShapes[clone.id] || this.app.getShape(clone.id)) as TDShape
if (!current.point) throw Error('No point on that clone!')
nextShapes[clone.id] = {
...nextShapes[clone.id],
point: Vec.round(Vec.add(current.point, movement)),
}
})
} else {
// If not cloning...
@ -378,7 +389,6 @@ export class TranslateSession extends BaseSession {
if (!current.point) throw Error('No point on that clone!')
nextShapes[shape.id] = {
...nextShapes[shape.id],
point: Vec.round(Vec.add(current.point, movement)),
}
})

View file

@ -550,7 +550,6 @@ export class SelectTool extends BaseTool<Status> {
}
onRightPointShape: TLPointerEventHandler = (info) => {
console.log('right point shape')
if (!this.app.isSelected(info.target)) {
this.app.select(info.target)
}
@ -593,7 +592,6 @@ export class SelectTool extends BaseTool<Status> {
}
onRightPointBounds: TLPointerEventHandler = (info, e) => {
console.log('right point bounds')
e.stopPropagation()
}