replace undefined with nulls

spike-supabase
Steve Ruiz 2021-09-07 21:08:11 +01:00
rodzic 97ba2443e8
commit 5f3ee95cbf
28 zmienionych plików z 319 dodań i 149 usunięć

Wyświetl plik

@ -34,7 +34,7 @@ export interface TLHandle {
index: number
point: number[]
canBind?: boolean
bindingId?: string
bindingId: string | null
}
export interface TLShape {

Wyświetl plik

@ -30,7 +30,8 @@ export class Arrow extends TLDrawShapeUtil<ArrowShape> {
pathCache = new WeakMap<ArrowShape, string>()
defaultProps = {
id: 'id',
id: 'arrow_id',
nonce: 1,
type: TLDrawShapeType.Arrow as const,
name: 'Arrow',
parentId: 'page',

Wyświetl plik

@ -22,7 +22,8 @@ export class Draw extends TLDrawShapeUtil<DrawShape> {
polygonCache = new WeakMap<DrawShape['points'], string>([])
defaultProps: DrawShape = {
id: 'id',
id: 'draw_id',
nonce: 1,
type: TLDrawShapeType.Draw as const,
name: 'Draw',
parentId: 'page',

Wyświetl plik

@ -22,7 +22,8 @@ export class Ellipse extends TLDrawShapeUtil<EllipseShape> {
canBind = true
defaultProps = {
id: 'id',
id: 'ellipse_id',
nonce: 1,
type: TLDrawShapeType.Ellipse as const,
name: 'Ellipse',
parentId: 'page',

Wyświetl plik

@ -23,7 +23,8 @@ export class Group extends TLDrawShapeUtil<GroupShape> {
pathCache = new WeakMap<number[], string>([])
defaultProps: GroupShape = {
id: 'id',
id: 'group_id',
nonce: 1,
type: TLDrawShapeType.Group as const,
name: 'Group',
parentId: 'page',

Wyświetl plik

@ -23,7 +23,8 @@ export class Rectangle extends TLDrawShapeUtil<RectangleShape> {
pathCache = new WeakMap<number[], string>([])
defaultProps: RectangleShape = {
id: 'id',
id: 'rectangle_id',
nonce: 1,
type: TLDrawShapeType.Rectangle as const,
name: 'Rectangle',
parentId: 'page',

Wyświetl plik

@ -66,7 +66,8 @@ export class Text extends TLDrawShapeUtil<TextShape> {
pathCache = new WeakMap<number[], string>([])
defaultProps = {
id: 'id',
id: 'text_id',
nonce: 1,
type: TLDrawShapeType.Text as const,
name: 'Text',
parentId: 'page',

Wyświetl plik

@ -23,6 +23,7 @@ Array [
"childIndex": 1,
"id": "rect1",
"name": "Rectangle",
"nonce": 1487076708000,
"parentId": "page1",
"point": Array [
0,
@ -81,6 +82,7 @@ Array [
"childIndex": 1,
"id": "rect2",
"name": "Rectangle",
"nonce": 1487076708000,
"parentId": "page1",
"point": Array [
0,

Wyświetl plik

@ -5,11 +5,11 @@ import type { TLDrawShape, Data, TLDrawCommand } from '~types'
export function create(data: Data, shapes: TLDrawShape[]): TLDrawCommand {
const { currentPageId } = data.appState
const beforeShapes: Record<string, Patch<TLDrawShape> | undefined> = {}
const afterShapes: Record<string, Patch<TLDrawShape> | undefined> = {}
const beforeShapes: Record<string, Patch<TLDrawShape> | null> = {}
const afterShapes: Record<string, Patch<TLDrawShape> | null> = {}
shapes.forEach((shape) => {
beforeShapes[shape.id] = undefined
beforeShapes[shape.id] = null
afterShapes[shape.id] = shape
})

Wyświetl plik

@ -31,10 +31,10 @@ export function duplicatePage(data: Data, pageId: string): TLDrawCommand {
},
document: {
pages: {
[newId]: undefined,
[newId]: null,
},
pageStates: {
[newId]: undefined,
[newId]: null,
},
},
},
@ -53,10 +53,10 @@ export function duplicatePage(data: Data, pageId: string): TLDrawCommand {
selectedIds: [],
camera: { point: [-window.innerWidth / 2, -window.innerHeight / 2], zoom: 1 },
currentParentId: newId,
editingId: undefined,
bindingId: undefined,
hoveredId: undefined,
pointedId: undefined,
editingId: null,
bindingId: null,
hoveredId: null,
pointedId: null,
},
},
},

Wyświetl plik

@ -31,7 +31,7 @@ export function duplicate(data: Data, ids: string[]): TLDrawCommand {
.filter((shape) => !ids.includes(shape.parentId))
.forEach((shape) => {
const duplicatedId = Utils.uniqueId()
before.shapes[duplicatedId] = undefined
before.shapes[duplicatedId] = null
after.shapes[duplicatedId] = {
...Utils.deepClone(shape),
@ -68,7 +68,7 @@ export function duplicate(data: Data, ids: string[]): TLDrawCommand {
const child = TLDR.getShape(data, childId, currentPageId)
const duplicatedId = Utils.uniqueId()
const duplicatedParentId = duplicateMap[shape.id]
before.shapes[duplicatedId] = undefined
before.shapes[duplicatedId] = null
after.shapes[duplicatedId] = {
...Utils.deepClone(child),
id: duplicatedId,
@ -102,7 +102,7 @@ export function duplicate(data: Data, ids: string[]): TLDrawCommand {
toId: duplicateMap[binding.toId],
}
before.bindings[duplicatedBindingId] = undefined
before.bindings[duplicatedBindingId] = null
after.bindings[duplicatedBindingId] = duplicatedBinding
// Change the duplicated shape's handle so that it reference
@ -119,7 +119,7 @@ export function duplicate(data: Data, ids: string[]): TLDrawCommand {
const boundShape = after.shapes[duplicateMap[binding.fromId]]
Object.values(boundShape!.handles!).forEach((handle) => {
if (handle!.bindingId === binding.id) {
handle!.bindingId = undefined
handle!.bindingId = null
}
})
}

Wyświetl plik

@ -10,11 +10,11 @@ export function group(
groupId: string,
pageId: string
): TLDrawCommand | undefined {
const beforeShapes: Record<string, Patch<TLDrawShape | undefined>> = {}
const afterShapes: Record<string, Patch<TLDrawShape | undefined>> = {}
const beforeShapes: Record<string, Patch<TLDrawShape | null>> = {}
const afterShapes: Record<string, Patch<TLDrawShape | null>> = {}
const beforeBindings: Record<string, Patch<TLDrawBinding | undefined>> = {}
const afterBindings: Record<string, Patch<TLDrawBinding | undefined>> = {}
const beforeBindings: Record<string, Patch<TLDrawBinding | null>> = {}
const afterBindings: Record<string, Patch<TLDrawBinding | null>> = {}
const idsToGroup = [...ids]
const shapesToGroup: TLDrawShape[] = []
@ -24,7 +24,7 @@ export function group(
// Collect all of the shapes to group (and their ids)
for (const id of ids) {
const shape = TLDR.getShape(data, id, pageId)
if (shape.children === undefined) {
if (!shape.children) {
shapesToGroup.push(shape)
} else {
otherEffectedGroups.push(shape)
@ -74,7 +74,7 @@ export function group(
const groupBounds = Utils.getCommonBounds(shapesToGroup.map((shape) => TLDR.getBounds(shape)))
// Create the group
beforeShapes[groupId] = undefined
beforeShapes[groupId] = null
afterShapes[groupId] = TLDR.getShapeUtils({ type: TLDrawShapeType.Group } as TLDrawShape).create({
id: groupId,
@ -119,7 +119,7 @@ export function group(
// If the parent has no children, remove it
if (nextChildren.length === 0) {
beforeShapes[shape.id] = shape
afterShapes[shape.id] = undefined
afterShapes[shape.id] = null
// And if that parent is part of a different group, mark it for cleanup
// (This is necessary only when we implement nested groups.)
@ -148,10 +148,10 @@ export function group(
Object.values(page.bindings).forEach((binding) => {
for (const id of [binding.toId, binding.fromId]) {
// If the binding references a deleted shape...
if (afterShapes[id] === undefined) {
if (!afterShapes[id]) {
// Delete this binding
beforeBindings[binding.id] = binding
afterBindings[binding.id] = undefined
afterBindings[binding.id] = null
// Let's also look each the bound shape...
const shape = TLDR.getShape(data, id, pageId)
@ -177,7 +177,7 @@ export function group(
...afterShapes[id],
handles: {
...afterShapes[id]?.handles,
[handle.id]: { bindingId: undefined },
[handle.id]: { bindingId: null },
},
}
}

Wyświetl plik

@ -44,7 +44,7 @@ export function moveToPage(
.forEach((shape) => {
movingShapeIds.add(shape.id)
shapesToMove.add(shape)
if (shape.children !== undefined) {
if (shape.children) {
shape.children.forEach((childId) => {
movingShapeIds.add(childId)
shapesToMove.add(TLDR.getShape(data, childId, fromPageId))
@ -61,10 +61,10 @@ export function moveToPage(
movingShapes.forEach((shape, i) => {
// Remove the shape from the fromPage
fromPage.before.shapes[shape.id] = shape
fromPage.after.shapes[shape.id] = undefined
fromPage.after.shapes[shape.id] = null
// But the moved shape on the "to" page
toPage.before.shapes[shape.id] = undefined
toPage.before.shapes[shape.id] = null
toPage.after.shapes[shape.id] = shape
// If the shape's parent isn't moving too, reparent the shape to
@ -98,7 +98,7 @@ export function moveToPage(
// Always delete the binding from the from page
fromPage.before.bindings[binding.id] = binding
fromPage.after.bindings[binding.id] = undefined
fromPage.after.bindings[binding.id] = null
// Delete the reference from the binding's fromShape
@ -110,7 +110,7 @@ export function moveToPage(
if (shouldCopy) {
// Just move the binding to the new page
toPage.before.bindings[binding.id] = undefined
toPage.before.bindings[binding.id] = null
toPage.after.bindings[binding.id] = binding
} else {
if (movingShapeIds.has(binding.fromId)) {

Wyświetl plik

@ -36,6 +36,7 @@ describe('Toggle decoration command', () => {
TLDR.getShapeUtils({ type: 'arrow' } as TLDrawShape).create({
id: 'arrow1',
parentId: 'page1',
nonce: Date.now(),
})
)
.select('arrow1')

Wyświetl plik

@ -34,7 +34,7 @@ export function translate(data: Data, ids: string[], delta: number[]): TLDrawCom
bindingsToDelete.forEach((binding) => {
before.bindings[binding.id] = binding
after.bindings[binding.id] = undefined
after.bindings[binding.id] = null
for (const id of [binding.toId, binding.fromId]) {
// Let's also look at the bound shape...

Wyświetl plik

@ -1,14 +1,13 @@
import type { GroupShape, TLDrawBinding, TLDrawShape } from '~types'
import type { Data, TLDrawCommand } from '~types'
import type { Data, TLDrawCommand, Patch } from '~types'
import { TLDR } from '~state/tldr'
import type { Patch } from 'rko'
export function ungroup(data: Data, groupId: string, pageId: string): TLDrawCommand | undefined {
const beforeShapes: Record<string, Patch<TLDrawShape | undefined>> = {}
const afterShapes: Record<string, Patch<TLDrawShape | undefined>> = {}
const beforeShapes: Record<string, Patch<TLDrawShape> | null> = {}
const afterShapes: Record<string, Patch<TLDrawShape> | null> = {}
const beforeBindings: Record<string, Patch<TLDrawBinding | undefined>> = {}
const afterBindings: Record<string, Patch<TLDrawBinding | undefined>> = {}
const beforeBindings: Record<string, Patch<TLDrawBinding> | null> = {}
const afterBindings: Record<string, Patch<TLDrawBinding> | null> = {}
// The group shape
const groupShape = TLDR.getShape<GroupShape>(data, groupId, pageId)
@ -36,7 +35,7 @@ export function ungroup(data: Data, groupId: string, pageId: string): TLDrawComm
// Remove the group shape
beforeShapes[groupId] = groupShape
afterShapes[groupId] = undefined
afterShapes[groupId] = null
// Reparent shapes to the page
sortedShapes.forEach((shape, index) => {
@ -59,10 +58,10 @@ export function ungroup(data: Data, groupId: string, pageId: string): TLDrawComm
.forEach((binding) => {
for (const id of [binding.toId, binding.fromId]) {
// If the binding references the deleted group...
if (afterShapes[id] === undefined) {
if (!afterShapes[id]) {
// Delete the binding
beforeBindings[binding.id] = binding
afterBindings[binding.id] = undefined
afterBindings[binding.id] = null
// Let's also look each the bound shape...
const shape = TLDR.getShape(data, id, pageId)
@ -88,7 +87,7 @@ export function ungroup(data: Data, groupId: string, pageId: string): TLDrawComm
...afterShapes[id],
handles: {
...afterShapes[id]?.handles,
[handle.id]: { bindingId: undefined },
[handle.id]: { bindingId: null },
},
}
}

Wyświetl plik

@ -22,16 +22,16 @@ export function removeShapesFromPage(data: Data, ids: string[], pageId: string)
deletedIds.add(id)
const shape = TLDR.getShape(data, id, pageId)
before.shapes[id] = shape
after.shapes[id] = undefined
after.shapes[id] = null
// Also delete the shape's children
if (shape.children !== undefined) {
if (shape.children) {
shape.children.forEach((childId) => {
deletedIds.add(childId)
const child = TLDR.getShape(data, childId, pageId)
before.shapes[childId] = child
after.shapes[childId] = undefined
after.shapes[childId] = null
})
}
@ -57,10 +57,10 @@ export function removeShapesFromPage(data: Data, ids: string[], pageId: string)
.forEach((binding) => {
for (const id of [binding.toId, binding.fromId]) {
// If the binding references a deleted shape...
if (after.shapes[id] === undefined) {
if (!after.shapes[id]) {
// Delete this binding
before.bindings[binding.id] = binding
after.bindings[binding.id] = undefined
after.bindings[binding.id] = null
// Let's also look each the bound shape...
const shape = TLDR.getShape(data, id, pageId)

Wyświetl plik

@ -7,7 +7,7 @@ import {
Session,
TLDrawStatus,
} from '~types'
import { Vec, Utils, TLHandle } from '@tldraw/core'
import { Vec, Utils } from '@tldraw/core'
import { TLDR } from '~state/tldr'
export class ArrowSession implements Session {
@ -40,7 +40,7 @@ export class ArrowSession implements Session {
this.initialBinding = page.bindings[initialBindingId]
} else {
// Explicitly set this handle to undefined, so that it gets deleted on undo
this.initialShape.handles[this.handleId].bindingId = undefined
this.initialShape.handles[this.handleId].bindingId = null
}
}
@ -82,7 +82,7 @@ export class ArrowSession implements Session {
// If nothing changes, we want this to be the same object reference as
// before. If it does change, we'll redefine this later on.
let nextBindings: Record<string, TLDrawBinding | undefined> = page.bindings
let nextBindings: Record<string, TLDrawBinding | null> = page.bindings
// If the handle can bind, then we need to search bindable shapes for
// a binding. If the handle already has a binding, then we will either
@ -99,8 +99,8 @@ export class ArrowSession implements Session {
// metaKey
// )
let binding: ArrowBinding | undefined = undefined
let target: TLDrawShape | undefined = undefined
let binding: ArrowBinding | null = null
let target: TLDrawShape | null = null
// Alt key skips binding
if (!altKey) {
@ -153,21 +153,21 @@ export class ArrowSession implements Session {
}
// If we didn't find a target...
if (binding === undefined) {
if (!binding) {
this.didBind = false
if (handle.bindingId) {
nextBindings = { ...nextBindings }
nextBindings[handle.bindingId] = undefined
nextBindings[handle.bindingId] = null
}
nextShape.handles[handleId].bindingId = undefined
nextShape.handles[handleId].bindingId = null
} else if (target) {
this.didBind = true
nextBindings = { ...nextBindings }
if (handle.bindingId && handle.bindingId !== this.newBindingId) {
nextBindings[handle.bindingId] = undefined
nextShape.handles[handleId].bindingId = undefined
nextBindings[handle.bindingId] = null
nextShape.handles[handleId].bindingId = null
}
// If we found a new binding, add its id to the shape's handle...
@ -254,8 +254,8 @@ export class ArrowSession implements Session {
const { initialShape, initialBinding, handleId } = this
const page = TLDR.getPage(data, data.appState.currentPageId)
const beforeBindings: Partial<Record<string, TLDrawBinding>> = {}
const afterBindings: Partial<Record<string, TLDrawBinding>> = {}
const beforeBindings: Partial<Record<string, TLDrawBinding | null>> = {}
const afterBindings: Partial<Record<string, TLDrawBinding | null>> = {}
const currentShape = TLDR.getShape<ArrowShape>(
data,
@ -266,11 +266,11 @@ export class ArrowSession implements Session {
if (initialBinding) {
beforeBindings[initialBinding.id] = initialBinding
afterBindings[initialBinding.id] = undefined
afterBindings[initialBinding.id] = null
}
if (currentBindingId) {
beforeBindings[currentBindingId] = undefined
beforeBindings[currentBindingId] = null
afterBindings[currentBindingId] = page.bindings[currentBindingId]
}

Wyświetl plik

@ -115,7 +115,7 @@ export function getBrushSnapshot(data: Data) {
(shape) =>
!(
shape.isHidden ||
shape.children !== undefined ||
shape.children ||
selectedIds.includes(shape.id) ||
selectedIds.includes(shape.parentId)
)

Wyświetl plik

@ -11,19 +11,11 @@ describe('Draw session', () => {
expect(tlstate.getShape('draw1')).toBe(undefined)
tlstate
.create({
.createShapes({
id: 'draw1',
parentId: 'page1',
name: 'Draw',
childIndex: 5,
type: TLDrawShapeType.Draw,
point: [32, 32],
points: [[0, 0]],
style: {
dash: DashStyle.Draw,
size: SizeStyle.Medium,
color: ColorStyle.Blue,
},
})
.select('draw1')
.startDrawSession('draw1', [0, 0])

Wyświetl plik

@ -153,7 +153,7 @@ export function getRotateSnapshot(data: Data) {
boundsRotation: pageState.boundsRotation || 0,
commonBoundsCenter,
initialShapes: initialShapes
.filter((shape) => shape.children === undefined)
.filter((shape) => !shape.children)
.map((shape) => {
const bounds = TLDR.getBounds(shape)
const center = Utils.getBoundsCenter(bounds)

Wyświetl plik

@ -34,7 +34,7 @@ export class TranslateSession implements Session {
const nextBindings: Patch<Record<string, TLDrawBinding>> = {}
bindingsToDelete.forEach((binding) => (nextBindings[binding.id] = undefined))
bindingsToDelete.forEach((binding) => (nextBindings[binding.id] = null))
return {
document: {
@ -134,11 +134,11 @@ export class TranslateSession implements Session {
// Delete the bindings
bindingsToDelete.forEach((binding) => (nextBindings[binding.id] = undefined))
bindingsToDelete.forEach((binding) => (nextBindings[binding.id] = null))
// Delete the clones
clones.forEach((clone) => {
nextShapes[clone.id] = undefined
nextShapes[clone.id] = null
if (clone.parentId !== currentPageId) {
nextShapes[clone.parentId] = {
...nextShapes[clone.parentId],
@ -156,7 +156,7 @@ export class TranslateSession implements Session {
// Delete the cloned bindings
for (const binding of this.snapshot.clonedBindings) {
nextBindings[binding.id] = undefined
nextBindings[binding.id] = null
}
// Set selected ids
@ -195,8 +195,8 @@ export class TranslateSession implements Session {
cancel = (data: Data) => {
const { initialShapes, clones, clonedBindings, bindingsToDelete } = this.snapshot
const nextBindings: Record<string, Partial<TLDrawBinding> | undefined> = {}
const nextShapes: Record<string, Partial<TLDrawShape> | undefined> = {}
const nextBindings: Record<string, Partial<TLDrawBinding> | null> = {}
const nextShapes: Record<string, Partial<TLDrawShape> | null> = {}
const nextPageState: Partial<TLPageState> = {}
// Put back any deleted bindings
@ -206,10 +206,10 @@ export class TranslateSession implements Session {
initialShapes.forEach(({ id, point }) => (nextShapes[id] = { ...nextShapes[id], point }))
// Delete clones
clones.forEach((clone) => (nextShapes[clone.id] = undefined))
clones.forEach((clone) => (nextShapes[clone.id] = null))
// Delete cloned bindings
clonedBindings.forEach((binding) => (nextBindings[binding.id] = undefined))
clonedBindings.forEach((binding) => (nextBindings[binding.id] = null))
nextPageState.selectedIds = this.snapshot.selectedIds
@ -243,7 +243,7 @@ export class TranslateSession implements Session {
if (this.isCloning) {
// Update the clones
clones.forEach((clone) => {
beforeShapes[clone.id] = undefined
beforeShapes[clone.id] = null
afterShapes[clone.id] = TLDR.getShape(data, clone.id, pageId)
@ -262,7 +262,7 @@ export class TranslateSession implements Session {
// Update the cloned bindings
clonedBindings.forEach((binding) => {
beforeBindings[binding.id] = undefined
beforeBindings[binding.id] = null
afterBindings[binding.id] = TLDR.getBinding(data, binding.id, pageId)
})
} else {
@ -399,7 +399,7 @@ export function getTranslateSnapshot(data: Data) {
})
clones.forEach((clone) => {
if (clone.children !== undefined) {
if (clone.children) {
clone.children = clone.children.map((childId) => cloneMap[childId])
}
})
@ -445,7 +445,7 @@ export function getTranslateSnapshot(data: Data) {
if (clone.handles) {
for (const id in clone.handles) {
const handle = clone.handles[id as keyof ArrowShape['handles']]
handle.bindingId = handle.bindingId ? clonedBindingsMap[handle.bindingId] : undefined
handle.bindingId = handle.bindingId ? clonedBindingsMap[handle.bindingId] : null
}
}
}

Wyświetl plik

@ -39,6 +39,8 @@ import {
TLDrawBinding,
GroupShape,
TLDrawCommand,
TLDrawPatch,
PagePartial,
} from '~types'
import { TLDR } from './tldr'
import { defaultStyle } from '~shape'
@ -72,7 +74,7 @@ const initialData: Data = {
settings: {
isPenMode: false,
isDarkMode: false,
isZoomSnap: true,
isZoomSnap: false,
isDebugMode: process.env.NODE_ENV === 'development',
isReadonlyMode: false,
nudgeDistanceLarge: 10,
@ -162,12 +164,16 @@ export class TLDrawState extends StateManager<Data> {
protected cleanup = (state: Data, prev: Data, patch: Patch<Data>, reason?: string): Data => {
const data = { ...state }
this._onPatch?.(this, reason || 'patch', patch)
const isMergingExternalPatch = reason !== 'patch:merge'
if (reason !== 'patch:merge') {
this._onPatch?.(this, reason || 'patch', patch)
}
// 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 (page === undefined) {
if (!page) {
// If page is undefined, delete the page and pagestate
delete data.document.pages[pageId]
delete data.document.pageStates[pageId]
@ -177,50 +183,55 @@ export class TLDrawState extends StateManager<Data> {
const prevPage = prev.document.pages[pageId]
if (!prevPage || page.shapes !== prevPage.shapes || page.bindings !== prevPage.bindings) {
// Work through the shapes and bindings
page.shapes = { ...page.shapes }
page.bindings = { ...page.bindings }
// And collect groups that need to be updated or removed
const groupsToUpdate = new Set<GroupShape>()
// If shape is undefined, delete the shape
Object.keys(page.shapes).forEach((id) => {
// Find which shapes have changed
const changedShapeIds = Object.keys(page.shapes).filter(
(id) => prevPage?.shapes[id] !== page.shapes[id]
)
changedShapeIds.forEach((id) => {
const shape = page.shapes[id]
let parentId: string
const parentId = shape?.parentId || prevPage.shapes[id]?.parentId
if (!shape) {
parentId = prevPage.shapes[id]?.parentId
// If shape is undefined, delete the shape
delete page.shapes[id]
} else {
parentId = shape.parentId
if (!isMergingExternalPatch) {
// Update the shape's nonce
shape.nonce = Date.now()
}
}
// If the shape is the child of a group, then update the group
// (unless the group is being deleted too)
if (parentId && parentId !== pageId) {
const group = page.shapes[parentId]
if (group !== undefined) {
groupsToUpdate.add(page.shapes[parentId] as GroupShape)
}
if (parentId !== pageId) {
const group = page.shapes[parentId] as GroupShape | null
if (group) groupsToUpdate.add(group)
}
})
// If binding is undefined, delete the binding
// Scan the page's bindings, looking for bindings to delete
Object.keys(page.bindings).forEach((id) => {
if (!page.bindings[id]) delete page.bindings[id]
})
// Find which shapes have changed
const changedShapeIds = Object.values(page.shapes)
.filter((shape) => prevPage?.shapes[shape.id] !== shape)
.map((shape) => shape.id)
data.document.pages[pageId] = page
// Get bindings related to the changed shapes
const bindingsToUpdate = TLDR.getRelatedBindings(data, changedShapeIds, pageId)
// Update all of the bindings we've just collected
bindingsToUpdate.forEach((binding) => {
// Update the nonce
if (!isMergingExternalPatch) {
binding.nonce = Date.now()
}
const toShape = page.shapes[binding.toId]
const fromShape = page.shapes[binding.fromId]
const toUtils = TLDR.getShapeUtils(toShape)
@ -248,7 +259,7 @@ export class TLDrawState extends StateManager<Data> {
groupsToUpdate.forEach((group) => {
if (!group) throw Error('no group!')
const children = group.children.filter((id) => page.shapes[id] !== undefined)
const children = group.children.filter((id) => !!page.shapes[id])
const commonBounds = Utils.getCommonBounds(
children
@ -264,6 +275,8 @@ export class TLDrawState extends StateManager<Data> {
children,
}
})
data.document.pages[pageId] = page
}
// Clean up page state, preventing hovers on deleted shapes
@ -286,6 +299,10 @@ export class TLDrawState extends StateManager<Data> {
delete nextPageState.editingId
}
// Delete any new selected ids
nextPageState.selectedIds = nextPageState.selectedIds.filter((id) => page.shapes[id])
data.document.pageStates[pageId] = nextPageState
})
}
@ -322,7 +339,9 @@ export class TLDrawState extends StateManager<Data> {
this.clearSelectHistory()
}
this._onChange?.(this, id)
if (id !== 'patch:merge') {
this._onChange?.(this, id)
}
}
/**
@ -490,9 +509,103 @@ export class TLDrawState extends StateManager<Data> {
* @param document
*/
mergeDocument = (document: TLDrawDocument): this => {
const next = { ...this.state }
next.document.pages[next.appState.currentPageId] = document.pages[next.appState.currentPageId]
return this.replaceState(next, 'merge')
const currentPageId = this.state.appState.currentPageId
const mergingPage = document.pages[currentPageId]
const currentPage = this.state.document.pages[currentPageId]
const nextPage: TLDrawPage = {
id: currentPageId,
shapes: {},
bindings: {},
}
let didChange = false
Object.entries(mergingPage.shapes).forEach(([id, shape]) => {
if (currentPage.shapes[id] !== shape) {
didChange = true
nextPage.shapes[id] = shape
}
})
Object.entries(mergingPage.bindings).forEach(([id, binding]) => {
if (currentPage.bindings[id] !== binding) {
didChange = true
nextPage.bindings[id] = binding
}
})
if (!didChange) return this
return this.replaceState(
{
...this.state,
document: {
...this.document,
pages: {
...this.document.pages,
[currentPageId]: nextPage,
},
},
},
'merge'
)
}
mergePatch = (patch: TLDrawPatch): this => {
const currentPageId = this.state.appState.currentPageId
const mergingPage = patch.document?.pages?.[currentPageId]
const currentPage = this.state.document.pages[currentPageId]
const patchPage: PagePartial = {
shapes: {},
bindings: {},
}
let didChange = false
if (mergingPage) {
Object.entries(mergingPage?.shapes || {}).forEach(([id, shape]) => {
if (currentPage.shapes[id] !== shape) {
didChange = true
patchPage.shapes[id] = shape
}
})
Object.entries(mergingPage?.bindings || {}).forEach(([id, binding]) => {
if (currentPage.bindings[id] !== binding) {
didChange = true
patchPage.bindings[id] = binding
}
})
}
if (!didChange) return this
return this.patchState(
{
document: {
pages: {
[currentPageId]: patchPage,
},
},
},
'merge'
)
}
// merge a set of patches
mergePatches = (patches: { time: number; patch: TLDrawPatch }[]): this => {
for (const patch of patches) {
setTimeout(() => {
this.mergePatch(patch.patch)
}, patch.time)
}
return this
}
/**
@ -848,14 +961,7 @@ export class TLDrawState extends StateManager<Data> {
this.getPagePoint([window.innerWidth / 2, window.innerHeight / 2])
)
this.create(
TLDR.getShapeUtils(TLDrawShapeType.Text).create({
id: Utils.uniqueId(),
parentId: this.appState.currentPageId,
childIndex,
point: [boundsCenter.minX, boundsCenter.minY],
})
)
this.create({ ...shape, point: [boundsCenter.minX, boundsCenter.minY] })
}
return this
@ -1647,6 +1753,7 @@ export class TLDrawState extends StateManager<Data> {
...shapes.map((shape) => {
return TLDR.getShapeUtils(shape as TLDrawShape).create({
...shape,
nonce: shape.nonce || Date.now(),
parentId: shape.parentId || this.currentPageId,
})
})
@ -2056,6 +2163,7 @@ export class TLDrawState extends StateManager<Data> {
shapes: {
[id]: utils.create({
id,
nonce: Date.now(),
parentId: this.currentPageId,
childIndex,
point: pagePoint,

Wyświetl plik

@ -8,6 +8,7 @@ export const mockDocument: TLDrawDocument = {
shapes: {
rect1: {
id: 'rect1',
nonce: 0,
parentId: 'page1',
name: 'Rectangle',
childIndex: 1,
@ -22,6 +23,7 @@ export const mockDocument: TLDrawDocument = {
},
rect2: {
id: 'rect2',
nonce: 0,
parentId: 'page1',
name: 'Rectangle',
childIndex: 2,
@ -36,6 +38,7 @@ export const mockDocument: TLDrawDocument = {
},
rect3: {
id: 'rect3',
nonce: 0,
parentId: 'page1',
name: 'Rectangle',
childIndex: 3,

Wyświetl plik

@ -4,7 +4,6 @@ import type { TLBinding, TLRenderInfo } from '@tldraw/core'
import { TLShape, TLShapeUtil, TLHandle } from '@tldraw/core'
import type { TLPage, TLPageState } from '@tldraw/core'
import type { StoreApi } from 'zustand'
import type { Command, Patch } from 'rko'
export type TLStore = StoreApi<Data>
@ -52,6 +51,18 @@ export interface Data {
}
}
export type Patch<T> = Partial<
{
[P in keyof T]: Patch<T[P]> | null
}
>
export interface Command<T extends object> {
id?: string
before: Patch<T>
after: Patch<T>
}
export type TLDrawPatch = Patch<Data>
export type TLDrawCommand = Command<Data>
@ -147,6 +158,7 @@ export enum Decoration {
}
export interface TLDrawBaseShape extends TLShape {
nonce?: number
style: ShapeStyles
type: TLDrawShapeType
}
@ -205,7 +217,11 @@ export abstract class TLDrawShapeUtil<T extends TLDrawShape> extends TLShapeUtil
export type TLDrawShapeUtils = Record<TLDrawShapeType, TLDrawShapeUtil<TLDrawShape>>
export interface ArrowBinding extends TLBinding {
export interface TLDrawBaseBinding extends TLBinding {
nonce?: number
}
export interface ArrowBinding extends TLDrawBaseBinding {
type: 'arrow'
handleId: keyof ArrowShape['handles']
distance: number

Wyświetl plik

@ -4,7 +4,7 @@ import Head from 'next/head'
import dynamic from 'next/dynamic'
import { supabase } from '-supabase/client'
import type { TLDrawProject } from '-types'
import type { TLDrawState } from '@tldraw/tldraw'
import type { TLDrawState, TLDrawPatch } from '@tldraw/tldraw'
import { Utils } from '@tldraw/core'
const Editor = dynamic(() => import('components/editor'), { ssr: false })
@ -15,40 +15,81 @@ interface RoomProps {
export default function Room({ id }: RoomProps): JSX.Element {
const rState = React.useRef<TLDrawState>(null)
const [ready, setReady] = React.useState(false)
const rTimer = React.useRef(0)
const rStartTime = React.useRef(0)
const rPendingPatches = React.useRef<{ time: number; patch: TLDrawPatch }[]>([])
const rUnsub = React.useRef<any>()
const userId = React.useRef(Utils.uniqueId())
React.useEffect(() => {
const sub = supabase
.from('projects')
.on('*', (payload) => {
if (payload.new.nonce !== userId.current) {
rState.current.mergeDocument(payload.new.document)
}
})
.subscribe()
return () => {
sub.unsubscribe()
clearTimeout(rTimer.current)
if (rUnsub.current) {
rUnsub.current?.()
}
}
}, [id])
}, [id, ready])
const handleMount = React.useCallback((tlstate: TLDrawState) => {
rState.current = tlstate
}, [])
const handleChange = React.useCallback(
(tlstate: TLDrawState, reason: string) => {
if (
!(reason.startsWith('command') || reason.startsWith('undo') || reason.startsWith('redo'))
) {
return
React.useEffect(() => {
supabase
.from<TLDrawProject>('projects')
.select('*')
.eq('id', id)
.then(({ data }) => {
const project = data[0]
if (project && rState.current) {
rState.current.loadDocument(project.document)
setReady(true)
}
})
const sub = supabase
.from('projects')
.on('*', (payload) => {
if (payload.new.nonce !== userId.current) {
rState.current.mergePatches(payload.new.patch)
}
})
.subscribe()
rUnsub.current = () => sub.unsubscribe()
return () => {
rUnsub.current?.()
}
})
const handlePatch = React.useCallback(
(tlstate: TLDrawState, reason: string, patch: TLDrawPatch) => {
if (rPendingPatches.current.length === 0) {
rStartTime.current = Date.now()
const handle = setTimeout(() => {
const patches = rPendingPatches.current
rPendingPatches.current = []
rStartTime.current = 0
supabase
.from<TLDrawProject>('projects')
.update({
document: tlstate.document,
patch: patches,
nonce: userId.current,
})
.eq('id', id)
.then(() => void null)
}, 100)
rTimer.current = handle as any
}
supabase
.from<TLDrawProject>('projects')
.update({ document: tlstate.document, nonce: userId.current })
.eq('id', id)
.then(() => void null)
rPendingPatches.current.push({ time: Date.now() - rStartTime.current, patch })
},
[id]
)
@ -58,7 +99,7 @@ export default function Room({ id }: RoomProps): JSX.Element {
<Head>
<title>tldraw</title>
</Head>
<Editor id={id} onChange={handleChange} onMount={handleMount} />
<Editor id={id} onPatch={handlePatch} onMount={handleMount} />
</>
)
}

Wyświetl plik

@ -1,8 +1,9 @@
import { TLDrawDocument } from '@tldraw/tldraw'
import { TLDrawDocument, TLDrawPatch } from '@tldraw/tldraw'
export interface TLDrawProject {
uuid: string
id: string
nonce: string
patch: { time: number; patch: TLDrawPatch }[]
document: TLDrawDocument
}

Wyświetl plik

@ -1,2 +1,3 @@
import '@testing-library/jest-dom/extend-expect'
import "fake-indexeddb/auto"
import 'fake-indexeddb/auto'
Date.now = jest.fn(() => 1487076708000) //14.02.2017