kopia lustrzana https://github.com/Tldraw/Tldraw
alex/auto-undo-redo: auto undo-redo
rodzic
b4f26556bf
commit
6d2018c897
|
@ -57,6 +57,7 @@ import {
|
|||
sortByIndex,
|
||||
structuredClone,
|
||||
} from '@tldraw/utils'
|
||||
import { setTimeout } from 'core-js'
|
||||
import { EventEmitter } from 'eventemitter3'
|
||||
import { flushSync } from 'react-dom'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
|
@ -552,9 +553,9 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
])
|
||||
}
|
||||
},
|
||||
beforeDelete: (record) => {
|
||||
afterDelete: (record) => {
|
||||
// page was deleted, need to check whether it's the current page and select another one if so
|
||||
if (this.getInstanceState().currentPageId === record.id) {
|
||||
if (this.getInstanceState()?.currentPageId === record.id) {
|
||||
const backupPageId = this.getPages().find((p) => p.id !== record.id)?.id
|
||||
if (backupPageId) {
|
||||
this.store.put([{ ...this.getInstanceState(), currentPageId: backupPageId }])
|
||||
|
@ -570,7 +571,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
},
|
||||
},
|
||||
instance: {
|
||||
beforeChange: (prev, next) => {
|
||||
afterChange: (prev, next) => {
|
||||
// instance should never be updated to a page that no longer exists (this can
|
||||
// happen when undoing a change that involves switching to a page that has since
|
||||
// been deleted by another user)
|
||||
|
@ -579,14 +580,15 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
? prev.currentPageId
|
||||
: this.getPages()[0]?.id
|
||||
if (backupPageId) {
|
||||
return { ...next, currentPageId: backupPageId }
|
||||
this.store.update(next.id, (instance) => ({
|
||||
...instance,
|
||||
currentPageId: backupPageId,
|
||||
}))
|
||||
} else {
|
||||
// if there are no pages, bail out to `ensureStoreIsUsable`
|
||||
this.store.ensureStoreIsUsable()
|
||||
return this.getInstanceState()
|
||||
}
|
||||
}
|
||||
return next
|
||||
},
|
||||
},
|
||||
instance_page_state: {
|
||||
|
@ -651,17 +653,16 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
)
|
||||
this.disposables.add(this.history.dispose)
|
||||
|
||||
this.store.ensureStoreIsUsable()
|
||||
this.history.ephemeral(() => {
|
||||
this.store.ensureStoreIsUsable()
|
||||
|
||||
// clear ephemeral state
|
||||
this._updateCurrentPageState(
|
||||
{
|
||||
// clear ephemeral state
|
||||
this._updateCurrentPageState({
|
||||
editingShapeId: null,
|
||||
hoveredShapeId: null,
|
||||
erasingShapeIds: [],
|
||||
},
|
||||
{ history: 'ephemeral' }
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
if (initialState && this.root.children[initialState] === undefined) {
|
||||
throw Error(`No state found for initialState "${initialState}".`)
|
||||
|
@ -1240,7 +1241,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
partial: Partial<Omit<TLInstance, 'currentPageId'>>,
|
||||
historyOptions?: TLHistoryBatchOptions
|
||||
): this {
|
||||
this._updateInstanceState(partial, historyOptions)
|
||||
this._updateInstanceState(partial, { history: 'ephemeral', ...historyOptions })
|
||||
|
||||
if (partial.isChangingStyle !== undefined) {
|
||||
clearTimeout(this._isChangingStyleTimeout)
|
||||
|
@ -2895,7 +2896,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
transact(() => {
|
||||
this.stopFollowingUser()
|
||||
|
||||
this.updateInstanceState({ followingUserId: userId }, { history: 'ephemeral' })
|
||||
this.updateInstanceState({ followingUserId: userId })
|
||||
})
|
||||
|
||||
const cancel = () => {
|
||||
|
@ -3000,7 +3001,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
* @public
|
||||
*/
|
||||
stopFollowingUser(): this {
|
||||
this.updateInstanceState({ followingUserId: null }, { history: 'ephemeral' })
|
||||
this.updateInstanceState({ followingUserId: null })
|
||||
this.emit('stop-following')
|
||||
return this
|
||||
}
|
||||
|
@ -6298,7 +6299,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
// find last parent id
|
||||
const currentPageShapesSorted = this.getCurrentPageShapesSorted()
|
||||
|
||||
let partials = shapes.map((partial) => {
|
||||
const partials = shapes.map((partial) => {
|
||||
if (!partial.id) {
|
||||
partial = { id: createShapeId(), ...partial }
|
||||
}
|
||||
|
@ -6310,7 +6311,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
// children of the creating shape's type, or else the page itself.
|
||||
if (
|
||||
!partial.parentId ||
|
||||
!(this.store.has(partial.parentId) || partials.some((p) => p.id === partial.parentId))
|
||||
!(this.store.has(partial.parentId) || shapes.some((p) => p.id === partial.parentId))
|
||||
) {
|
||||
let parentId: TLParentId = this.getFocusedGroupId()
|
||||
|
||||
|
@ -8058,12 +8059,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
|
||||
if (this.inputs.isPanning) {
|
||||
this.inputs.isPanning = false
|
||||
this.updateInstanceState({
|
||||
cursor: {
|
||||
type: this._prevCursor,
|
||||
rotation: 0,
|
||||
},
|
||||
})
|
||||
this.setCursor({ type: this._prevCursor, rotation: 0 })
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -152,14 +152,15 @@ export class HistoryManager<
|
|||
// start by collecting the pending diff (everything since the last mark).
|
||||
// we'll accumulate the diff to undo in this variable so we can apply it atomically.
|
||||
const pendingDiff = this.pendingDiff.clear()
|
||||
const isPendingDiffEmpty = isRecordsDiffEmpty(pendingDiff)
|
||||
const diffToUndo = reverseRecordsDiff(pendingDiff)
|
||||
|
||||
if (pushToRedoStack) {
|
||||
if (pushToRedoStack && !isPendingDiffEmpty) {
|
||||
redos = redos.push({ type: 'diff', diff: pendingDiff })
|
||||
}
|
||||
|
||||
let didFindMark = false
|
||||
if (isRecordsDiffEmpty(diffToUndo)) {
|
||||
if (isPendingDiffEmpty) {
|
||||
// if nothing has happened since the last mark, pop any intermediate marks off the stack
|
||||
while (undos.head?.type === 'stop') {
|
||||
const mark = undos.head
|
||||
|
@ -287,6 +288,17 @@ export class HistoryManager<
|
|||
this.stacks.set({ undos: stack(), redos: stack() })
|
||||
this.pendingDiff.clear()
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
debug() {
|
||||
const { undos, redos } = this.stacks.get()
|
||||
return {
|
||||
undos: undos.toArray(),
|
||||
redos: redos.toArray(),
|
||||
pendingDiff: this.pendingDiff.debug(),
|
||||
state: this.state,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const modeToState = {
|
||||
|
@ -314,4 +326,8 @@ class PendingDiff<R extends UnknownRecord> {
|
|||
squashRecordDiffsMutable(this.diff, [diff])
|
||||
this.isEmptyAtom.set(isRecordsDiffEmpty(this.diff))
|
||||
}
|
||||
|
||||
debug() {
|
||||
return { diff: this.diff, isEmpty: this.isEmpty() }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,6 +38,9 @@ export type ComputedCache<Data, R extends UnknownRecord> = {
|
|||
get(id: IdOf<R>): Data | undefined;
|
||||
};
|
||||
|
||||
// @internal (undocumented)
|
||||
export function createEmptyRecordsDiff<R extends UnknownRecord>(): RecordsDiff<R>;
|
||||
|
||||
// @public
|
||||
export function createRecordType<R extends UnknownRecord>(typeName: R['typeName'], config: {
|
||||
migrations?: Migrations;
|
||||
|
@ -229,13 +232,13 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
|
|||
schema: StoreSchema<R, Props>;
|
||||
props: Props;
|
||||
});
|
||||
// @internal
|
||||
accumulatingChanges(accumulator: RecordsDiff<R>, fn: () => void): void;
|
||||
// @internal (undocumented)
|
||||
addHistoryInterceptor(fn: (entry: HistoryEntry<R>, source: ChangeSource) => void): () => void;
|
||||
allRecords: () => R[];
|
||||
// (undocumented)
|
||||
applyDiff(diff: RecordsDiff<R>, runCallbacks?: boolean): void;
|
||||
// @internal (undocumented)
|
||||
atomic<T>(fn: () => T, runCallbacks?: boolean): T;
|
||||
clear: () => void;
|
||||
createComputedCache: <T, V extends R = R>(name: string, derive: (record: V) => T | undefined, isEqual?: ((a: V, b: V) => boolean) | undefined) => ComputedCache<T, V>;
|
||||
createSelectedComputedCache: <T, J, V extends R = R>(name: string, selector: (record: V) => T | undefined, derive: (input: T) => J | undefined) => ComputedCache<J, V>;
|
||||
|
|
|
@ -2,6 +2,7 @@ export type { BaseRecord, IdOf, RecordId, UnknownRecord } from './lib/BaseRecord
|
|||
export { IncrementalSetConstructor } from './lib/IncrementalSetConstructor'
|
||||
export { RecordType, assertIdType, createRecordType } from './lib/RecordType'
|
||||
export {
|
||||
createEmptyRecordsDiff,
|
||||
isRecordsDiffEmpty,
|
||||
reverseRecordsDiff,
|
||||
squashRecordDiffs,
|
||||
|
|
|
@ -12,6 +12,11 @@ export type RecordsDiff<R extends UnknownRecord> = {
|
|||
removed: Record<IdOf<R>, R>
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function createEmptyRecordsDiff<R extends UnknownRecord>(): RecordsDiff<R> {
|
||||
return { added: {}, updated: {}, removed: {} } as RecordsDiff<R>
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export function reverseRecordsDiff(diff: RecordsDiff<any>) {
|
||||
const result: RecordsDiff<any> = { added: diff.removed, removed: diff.added, updated: {} }
|
||||
|
|
|
@ -12,7 +12,7 @@ import { nanoid } from 'nanoid'
|
|||
import { IdOf, RecordId, UnknownRecord } from './BaseRecord'
|
||||
import { Cache } from './Cache'
|
||||
import { RecordScope } from './RecordType'
|
||||
import { RecordsDiff, squashRecordDiffs, squashRecordDiffsMutable } from './RecordsDiff'
|
||||
import { RecordsDiff, squashRecordDiffs } from './RecordsDiff'
|
||||
import { StoreQueries } from './StoreQueries'
|
||||
import { SerializedSchema, StoreSchema } from './StoreSchema'
|
||||
import { devFreeze } from './devFreeze'
|
||||
|
@ -350,7 +350,7 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
|
|||
* @public
|
||||
*/
|
||||
put = (records: R[], phaseOverride?: 'initialize'): void => {
|
||||
this.transactWithAfterEvents(() => {
|
||||
this.atomic(() => {
|
||||
const updates: Record<IdOf<UnknownRecord>, [from: R, to: R]> = {}
|
||||
const additions: Record<IdOf<UnknownRecord>, R> = {}
|
||||
|
||||
|
@ -447,7 +447,7 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
|
|||
* @public
|
||||
*/
|
||||
remove = (ids: IdOf<R>[]): void => {
|
||||
this.transactWithAfterEvents(() => {
|
||||
this.atomic(() => {
|
||||
const cancelled = [] as IdOf<R>[]
|
||||
const source = this.isMergingRemoteChanges ? 'remote' : 'user'
|
||||
|
||||
|
@ -586,7 +586,7 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
|
|||
throw new Error(`Failed to migrate snapshot: ${migrationResult.reason}`)
|
||||
}
|
||||
|
||||
transact(() => {
|
||||
this.atomic(() => {
|
||||
this.clear()
|
||||
this.put(Object.values(migrationResult.value))
|
||||
this.ensureStoreIsUsable()
|
||||
|
@ -708,24 +708,8 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Like {@link extractingChanges}, but accumulates the changes into the provided accumulator
|
||||
* diff instead of returning a fresh diff.
|
||||
* @internal
|
||||
*/
|
||||
accumulatingChanges(accumulator: RecordsDiff<R>, fn: () => void) {
|
||||
const changes: Array<RecordsDiff<R>> = []
|
||||
const dispose = this.historyAccumulator.addInterceptor((entry) => changes.push(entry.changes))
|
||||
try {
|
||||
transact(fn)
|
||||
squashRecordDiffsMutable(accumulator, changes)
|
||||
} finally {
|
||||
dispose()
|
||||
}
|
||||
}
|
||||
|
||||
applyDiff(diff: RecordsDiff<R>, runCallbacks = true) {
|
||||
this.transactWithAfterEvents(() => {
|
||||
this.atomic(() => {
|
||||
const toPut = objectMapValues(diff.added).concat(
|
||||
objectMapValues(diff.updated).map(([_from, to]) => to)
|
||||
)
|
||||
|
@ -847,7 +831,8 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
|
|||
this.pendingAfterEvents.set(id, { before, after, source })
|
||||
}
|
||||
}
|
||||
private transactWithAfterEvents<T>(fn: () => T, runCallbacks = true): T {
|
||||
/** @internal */
|
||||
atomic<T>(fn: () => T, runCallbacks = true): T {
|
||||
return transact(() => {
|
||||
if (this.pendingAfterEvents) return fn()
|
||||
|
||||
|
|
|
@ -224,9 +224,9 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
|||
isPrecise: boolean;
|
||||
}>;
|
||||
point: ObjectValidator< {
|
||||
type: "point";
|
||||
x: number;
|
||||
y: number;
|
||||
type: "point";
|
||||
}>;
|
||||
}, never>;
|
||||
end: UnionValidator<"type", {
|
||||
|
@ -238,9 +238,9 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
|||
isPrecise: boolean;
|
||||
}>;
|
||||
point: ObjectValidator< {
|
||||
type: "point";
|
||||
x: number;
|
||||
y: number;
|
||||
type: "point";
|
||||
}>;
|
||||
}, never>;
|
||||
bend: Validator<number>;
|
||||
|
@ -1061,9 +1061,9 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
|
|||
size: EnumStyleProp<"l" | "m" | "s" | "xl">;
|
||||
spline: EnumStyleProp<"cubic" | "line">;
|
||||
points: DictValidator<string, {
|
||||
id: string;
|
||||
x: number;
|
||||
y: number;
|
||||
id: string;
|
||||
index: IndexKey;
|
||||
}>;
|
||||
};
|
||||
|
|
|
@ -1645,7 +1645,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "<{\n type: \"point\";\n x: number;\n y: number;\n }>;\n }, never>;\n end: import(\"@tldraw/editor\")."
|
||||
"text": "<{\n x: number;\n y: number;\n type: \"point\";\n }>;\n }, never>;\n end: import(\"@tldraw/editor\")."
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
|
@ -1690,7 +1690,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "<{\n type: \"point\";\n x: number;\n y: number;\n }>;\n }, never>;\n bend: import(\"@tldraw/editor\")."
|
||||
"text": "<{\n x: number;\n y: number;\n type: \"point\";\n }>;\n }, never>;\n bend: import(\"@tldraw/editor\")."
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
|
@ -12394,7 +12394,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "<string, {\n x: number;\n y: number;\n id: string;\n index: import(\"@tldraw/editor\")."
|
||||
"text": "<string, {\n id: string;\n x: number;\n y: number;\n index: import(\"@tldraw/editor\")."
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
|
|
|
@ -11,7 +11,7 @@ afterEach(() => {
|
|||
})
|
||||
|
||||
describe(NoteShapeTool, () => {
|
||||
it('Creates note shapes on click-and-drag, supports undo and redo', () => {
|
||||
it.only('Creates note shapes on click-and-drag, supports undo and redo', () => {
|
||||
expect(editor.getCurrentPageShapes().length).toBe(0)
|
||||
|
||||
editor.setCurrentTool('note')
|
||||
|
@ -25,7 +25,6 @@ describe(NoteShapeTool, () => {
|
|||
|
||||
editor.cancel() // leave edit mode
|
||||
|
||||
editor.undo() // undoes the selection change
|
||||
editor.undo()
|
||||
|
||||
expect(editor.getCurrentPageShapes().length).toBe(0)
|
||||
|
|
|
@ -64,12 +64,7 @@ export class Cropping extends StateNode {
|
|||
if (!selectedShape) return
|
||||
|
||||
const cursorType = CursorTypeMap[this.info.handle!]
|
||||
this.editor.updateInstanceState({
|
||||
cursor: {
|
||||
type: cursorType,
|
||||
rotation: this.editor.getSelectionRotation(),
|
||||
},
|
||||
})
|
||||
this.editor.setCursor({ type: cursorType, rotation: this.editor.getSelectionRotation() })
|
||||
}
|
||||
|
||||
private getDefaultCrop = (): TLImageShapeCrop => ({
|
||||
|
|
|
@ -19,12 +19,7 @@ export class PointingCropHandle extends StateNode {
|
|||
if (!selectedShape) return
|
||||
|
||||
const cursorType = CursorTypeMap[this.info.handle!]
|
||||
this.editor.updateInstanceState({
|
||||
cursor: {
|
||||
type: cursorType,
|
||||
rotation: this.editor.getSelectionRotation(),
|
||||
},
|
||||
})
|
||||
this.editor.setCursor({ type: cursorType, rotation: this.editor.getSelectionRotation() })
|
||||
this.editor.setCroppingShape(selectedShape.id)
|
||||
}
|
||||
|
||||
|
|
|
@ -34,11 +34,9 @@ export class PointingResizeHandle extends StateNode {
|
|||
private updateCursor() {
|
||||
const selected = this.editor.getSelectedShapes()
|
||||
const cursorType = CursorTypeMap[this.info.handle!]
|
||||
this.editor.updateInstanceState({
|
||||
cursor: {
|
||||
type: cursorType,
|
||||
rotation: selected.length === 1 ? this.editor.getSelectionRotation() : 0,
|
||||
},
|
||||
this.editor.setCursor({
|
||||
type: cursorType,
|
||||
rotation: selected.length === 1 ? this.editor.getSelectionRotation() : 0,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -11,11 +11,9 @@ export class PointingRotateHandle extends StateNode {
|
|||
private info = {} as PointingRotateHandleInfo
|
||||
|
||||
private updateCursor() {
|
||||
this.editor.updateInstanceState({
|
||||
cursor: {
|
||||
type: CursorTypeMap[this.info.handle as RotateCorner],
|
||||
rotation: this.editor.getSelectionRotation(),
|
||||
},
|
||||
this.editor.setCursor({
|
||||
type: CursorTypeMap[this.info.handle as RotateCorner],
|
||||
rotation: this.editor.getSelectionRotation(),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -50,11 +50,9 @@ export class Rotating extends StateNode {
|
|||
})
|
||||
|
||||
// Update cursor
|
||||
this.editor.updateInstanceState({
|
||||
cursor: {
|
||||
type: CursorTypeMap[this.info.handle as RotateCorner],
|
||||
rotation: newSelectionRotation + this.snapshot.initialSelectionRotation,
|
||||
},
|
||||
this.editor.setCursor({
|
||||
type: CursorTypeMap[this.info.handle as RotateCorner],
|
||||
rotation: newSelectionRotation + this.snapshot.initialSelectionRotation,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -104,11 +102,9 @@ export class Rotating extends StateNode {
|
|||
})
|
||||
|
||||
// Update cursor
|
||||
this.editor.updateInstanceState({
|
||||
cursor: {
|
||||
type: CursorTypeMap[this.info.handle as RotateCorner],
|
||||
rotation: newSelectionRotation + this.snapshot.initialSelectionRotation,
|
||||
},
|
||||
this.editor.setCursor({
|
||||
type: CursorTypeMap[this.info.handle as RotateCorner],
|
||||
rotation: newSelectionRotation + this.snapshot.initialSelectionRotation,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -19,10 +19,7 @@ export class ZoomTool extends StateNode {
|
|||
|
||||
override onExit = () => {
|
||||
this.parent.setCurrentToolIdMask(undefined)
|
||||
this.editor.updateInstanceState(
|
||||
{ zoomBrush: null, cursor: { type: 'default', rotation: 0 } },
|
||||
{ history: 'ephemeral' }
|
||||
)
|
||||
this.editor.updateInstanceState({ zoomBrush: null, cursor: { type: 'default', rotation: 0 } })
|
||||
this.parent.setCurrentToolIdMask(undefined)
|
||||
}
|
||||
|
||||
|
|
|
@ -37,7 +37,7 @@ export function MobileStylePanel() {
|
|||
const handleStylesOpenChange = useCallback(
|
||||
(isOpen: boolean) => {
|
||||
if (!isOpen) {
|
||||
editor.updateInstanceState({ isChangingStyle: false }, { history: 'ephemeral' })
|
||||
editor.updateInstanceState({ isChangingStyle: false })
|
||||
}
|
||||
},
|
||||
[editor]
|
||||
|
|
|
@ -21,7 +21,7 @@ export const DefaultStylePanel = memo(function DefaultStylePanel({
|
|||
|
||||
const handlePointerOut = useCallback(() => {
|
||||
if (!isMobile) {
|
||||
editor.updateInstanceState({ isChangingStyle: false }, { history: 'ephemeral' })
|
||||
editor.updateInstanceState({ isChangingStyle: false })
|
||||
}
|
||||
}, [editor, isMobile])
|
||||
|
||||
|
|
|
@ -83,7 +83,7 @@ function useStyleChangeCallback() {
|
|||
editor.setStyleForSelectedShapes(style, value)
|
||||
}
|
||||
editor.setStyleForNextShapes(style, value)
|
||||
editor.updateInstanceState({ isChangingStyle: true }, { history: 'ephemeral' })
|
||||
editor.updateInstanceState({ isChangingStyle: true })
|
||||
})
|
||||
|
||||
trackEvent('set-style', { source: 'style-panel', id: style.id, value: value as string })
|
||||
|
@ -332,7 +332,7 @@ export function OpacitySlider() {
|
|||
editor.setOpacityForSelectedShapes(item)
|
||||
}
|
||||
editor.setOpacityForNextShapes(item)
|
||||
editor.updateInstanceState({ isChangingStyle: true }, { history: 'ephemeral' })
|
||||
editor.updateInstanceState({ isChangingStyle: true })
|
||||
})
|
||||
|
||||
trackEvent('set-style', { source: 'style-panel', id: 'opacity', value })
|
||||
|
|
|
@ -1129,10 +1129,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
readonlyOk: true,
|
||||
onSelect(source) {
|
||||
trackEvent('toggle-transparent', { source })
|
||||
editor.updateInstanceState(
|
||||
{ exportBackground: !editor.getInstanceState().exportBackground },
|
||||
{ history: 'ephemeral' }
|
||||
)
|
||||
editor.updateInstanceState({
|
||||
exportBackground: !editor.getInstanceState().exportBackground,
|
||||
})
|
||||
},
|
||||
checkbox: true,
|
||||
},
|
||||
|
@ -1292,7 +1291,7 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
editor.setStyleForSelectedShapes(style, 'white')
|
||||
}
|
||||
editor.setStyleForNextShapes(style, 'white')
|
||||
editor.updateInstanceState({ isChangingStyle: true }, { history: 'ephemeral' })
|
||||
editor.updateInstanceState({ isChangingStyle: true })
|
||||
})
|
||||
trackEvent('set-style', { source, id: style.id, value: 'white' })
|
||||
},
|
||||
|
|
|
@ -24,9 +24,9 @@ export function pasteTldrawContent(editor: Editor, clipboard: TLContent, point?:
|
|||
seletionBoundsBefore?.collides(selectedBoundsAfter)
|
||||
) {
|
||||
// Creates a 'puff' to show a paste has happened.
|
||||
editor.updateInstanceState({ isChangingStyle: true }, { history: 'ephemeral' })
|
||||
editor.updateInstanceState({ isChangingStyle: true })
|
||||
setTimeout(() => {
|
||||
editor.updateInstanceState({ isChangingStyle: false }, { history: 'ephemeral' })
|
||||
editor.updateInstanceState({ isChangingStyle: false })
|
||||
}, 150)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -183,10 +183,7 @@ describe('<TldrawEditor />', () => {
|
|||
|
||||
expect(editor).toBeTruthy()
|
||||
await act(async () => {
|
||||
editor.updateInstanceState(
|
||||
{ screenBounds: { x: 0, y: 0, w: 1080, h: 720 } },
|
||||
{ history: 'ephemeral' }
|
||||
)
|
||||
editor.updateInstanceState({ screenBounds: { x: 0, y: 0, w: 1080, h: 720 } })
|
||||
})
|
||||
|
||||
const id = createShapeId()
|
||||
|
@ -304,10 +301,7 @@ describe('Custom shapes', () => {
|
|||
|
||||
expect(editor).toBeTruthy()
|
||||
await act(async () => {
|
||||
editor.updateInstanceState(
|
||||
{ screenBounds: { x: 0, y: 0, w: 1080, h: 720 } },
|
||||
{ history: 'ephemeral' }
|
||||
)
|
||||
editor.updateInstanceState({ screenBounds: { x: 0, y: 0, w: 1080, h: 720 } })
|
||||
})
|
||||
|
||||
expect(editor.shapeUtils.card).toBeTruthy()
|
||||
|
|
|
@ -23,7 +23,7 @@ describe('when less than two shapes are selected', () => {
|
|||
editor.setSelectedShapes([ids.boxB])
|
||||
|
||||
const fn = jest.fn()
|
||||
editor.on('update', fn)
|
||||
editor.store.listen(fn)
|
||||
editor.alignShapes(editor.getSelectedShapeIds(), 'top')
|
||||
jest.advanceTimersByTime(1000)
|
||||
expect(fn).not.toHaveBeenCalled()
|
||||
|
|
|
@ -46,7 +46,7 @@ describe('distributeShapes command', () => {
|
|||
it('does nothing', () => {
|
||||
editor.setSelectedShapes([ids.boxA, ids.boxB])
|
||||
const fn = jest.fn()
|
||||
editor.on('change-history', fn)
|
||||
editor.store.listen(fn)
|
||||
editor.distributeShapes(editor.getSelectedShapeIds(), 'horizontal')
|
||||
jest.advanceTimersByTime(1000)
|
||||
expect(fn).not.toHaveBeenCalled()
|
||||
|
|
|
@ -60,8 +60,9 @@ describe('Editor.moveShapesToPage', () => {
|
|||
|
||||
it('Adds undo items', () => {
|
||||
editor.history.clear()
|
||||
expect(editor.history.getNumUndos()).toBe(0)
|
||||
editor.moveShapesToPage([ids.box1], ids.page2)
|
||||
expect(editor.history.getNumUndos()).toBeGreaterThan(1)
|
||||
expect(editor.history.getNumUndos()).toBe(1)
|
||||
})
|
||||
|
||||
it('Does nothing on an empty ids array', () => {
|
||||
|
|
|
@ -52,7 +52,7 @@ describe('distributeShapes command', () => {
|
|||
it('does nothing', () => {
|
||||
editor.setSelectedShapes([ids.boxA, ids.boxB])
|
||||
const fn = jest.fn()
|
||||
editor.on('change-history', fn)
|
||||
editor.store.listen(fn)
|
||||
editor.stackShapes(editor.getSelectedShapeIds(), 'horizontal', 0)
|
||||
jest.advanceTimersByTime(1000)
|
||||
expect(fn).not.toHaveBeenCalled()
|
||||
|
|
|
@ -27,7 +27,7 @@ describe('when less than two shapes are selected', () => {
|
|||
it('does nothing', () => {
|
||||
editor.setSelectedShapes([ids.boxB])
|
||||
const fn = jest.fn()
|
||||
editor.on('change-history', fn)
|
||||
editor.store.listen(fn)
|
||||
editor.stretchShapes(editor.getSelectedShapeIds(), 'horizontal')
|
||||
jest.advanceTimersByTime(1000)
|
||||
|
||||
|
|
|
@ -0,0 +1,129 @@
|
|||
/* eslint-disable no-console */
|
||||
import { Editor, RecordsDiff } from '@tldraw/editor'
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import { DiffOptions, diff as jestDiff } from 'jest-diff'
|
||||
import { inspect } from 'util'
|
||||
|
||||
class Printer {
|
||||
private _output = ''
|
||||
private _indent = 0
|
||||
|
||||
appendLines(str: string) {
|
||||
const indent = ' '.repeat(this._indent)
|
||||
this._output +=
|
||||
str
|
||||
.split('\n')
|
||||
.map((line) => indent + line)
|
||||
.join('\n') + '\n'
|
||||
}
|
||||
|
||||
indent() {
|
||||
this._indent++
|
||||
}
|
||||
dedent() {
|
||||
this._indent--
|
||||
}
|
||||
|
||||
log(...args: any[]) {
|
||||
this.appendLines(args.map((arg) => (typeof arg === 'string' ? arg : inspect(arg))).join(' '))
|
||||
}
|
||||
|
||||
print() {
|
||||
console.log(this._output)
|
||||
}
|
||||
|
||||
get() {
|
||||
return this._output
|
||||
}
|
||||
}
|
||||
|
||||
export function prettyPrintDiff(diff: RecordsDiff<any>, opts?: DiffOptions) {
|
||||
const before = {} as Record<string, any>
|
||||
const after = {} as Record<string, any>
|
||||
|
||||
for (const added of Object.values(diff.added)) {
|
||||
after[added.id] = added
|
||||
}
|
||||
for (const [from, to] of Object.values(diff.updated)) {
|
||||
before[from.id] = from
|
||||
after[to.id] = to
|
||||
}
|
||||
for (const removed of Object.values(diff.removed)) {
|
||||
before[removed.id] = removed
|
||||
}
|
||||
|
||||
const prettyDiff = jestDiff(after, before, {
|
||||
aAnnotation: 'After',
|
||||
bAnnotation: 'Before',
|
||||
aIndicator: '+',
|
||||
bIndicator: '-',
|
||||
...opts,
|
||||
})
|
||||
|
||||
if (prettyDiff?.includes('Compared values have no visual difference.')) {
|
||||
const p = new Printer()
|
||||
p.log('Before & after have no visual difference.')
|
||||
p.log('Diff:')
|
||||
p.indent()
|
||||
p.log(diff)
|
||||
return p.get()
|
||||
}
|
||||
|
||||
return prettyDiff
|
||||
}
|
||||
|
||||
export function logHistory(editor: Editor) {
|
||||
const { undos, redos, pendingDiff } = editor.history.debug()
|
||||
const p = new Printer()
|
||||
p.log('=== History ===')
|
||||
p.indent()
|
||||
|
||||
p.log('Pending diff:')
|
||||
p.indent()
|
||||
if (pendingDiff.isEmpty) {
|
||||
p.log('(empty)')
|
||||
} else {
|
||||
p.log(prettyPrintDiff(pendingDiff.diff))
|
||||
}
|
||||
p.log('')
|
||||
p.dedent()
|
||||
|
||||
p.log('Undos:')
|
||||
p.indent()
|
||||
if (undos.length === 0) {
|
||||
p.log('(empty)\n')
|
||||
}
|
||||
for (const undo of undos) {
|
||||
if (!undo) continue
|
||||
if (undo.type === 'stop') {
|
||||
p.log('- Stop', undo.id)
|
||||
} else {
|
||||
p.log('- Diff')
|
||||
p.indent()
|
||||
p.log(prettyPrintDiff(undo.diff))
|
||||
p.dedent()
|
||||
}
|
||||
p.log('')
|
||||
}
|
||||
p.dedent()
|
||||
|
||||
p.log('Redos:')
|
||||
p.indent()
|
||||
if (redos.length === 0) {
|
||||
p.log('(empty)\n')
|
||||
}
|
||||
for (const redo of redos) {
|
||||
if (!redo) continue
|
||||
if (redo.type === 'stop') {
|
||||
p.log('> Stop', redo.id)
|
||||
} else {
|
||||
p.log('- Diff')
|
||||
p.indent()
|
||||
p.log(prettyPrintDiff(redo.diff))
|
||||
p.dedent()
|
||||
}
|
||||
p.log('')
|
||||
}
|
||||
|
||||
p.print()
|
||||
}
|
|
@ -47,9 +47,9 @@ export const arrowShapeProps: {
|
|||
isPrecise: boolean;
|
||||
} & {}>;
|
||||
point: T.ObjectValidator<{
|
||||
type: "point";
|
||||
x: number;
|
||||
y: number;
|
||||
type: "point";
|
||||
} & {}>;
|
||||
}, never>;
|
||||
end: T.UnionValidator<"type", {
|
||||
|
@ -61,9 +61,9 @@ export const arrowShapeProps: {
|
|||
isPrecise: boolean;
|
||||
} & {}>;
|
||||
point: T.ObjectValidator<{
|
||||
type: "point";
|
||||
x: number;
|
||||
y: number;
|
||||
type: "point";
|
||||
} & {}>;
|
||||
}, never>;
|
||||
bend: T.Validator<number>;
|
||||
|
@ -684,9 +684,9 @@ export const lineShapeProps: {
|
|||
size: EnumStyleProp<"l" | "m" | "s" | "xl">;
|
||||
spline: EnumStyleProp<"cubic" | "line">;
|
||||
points: T.DictValidator<string, {
|
||||
id: string;
|
||||
x: number;
|
||||
y: number;
|
||||
id: string;
|
||||
index: IndexKey;
|
||||
} & {}>;
|
||||
};
|
||||
|
|
|
@ -364,7 +364,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "<{\n type: \"point\";\n x: number;\n y: number;\n } & {}>;\n }, never>;\n end: "
|
||||
"text": "<{\n x: number;\n y: number;\n type: \"point\";\n } & {}>;\n }, never>;\n end: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
|
@ -409,7 +409,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "<{\n type: \"point\";\n x: number;\n y: number;\n } & {}>;\n }, never>;\n bend: "
|
||||
"text": "<{\n x: number;\n y: number;\n type: \"point\";\n } & {}>;\n }, never>;\n bend: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
|
@ -2818,7 +2818,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "<string, {\n x: number;\n y: number;\n id: string;\n index: "
|
||||
"text": "<string, {\n id: string;\n x: number;\n y: number;\n index: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
|
|
Ładowanie…
Reference in New Issue