diff --git a/CHANGELOG.md b/CHANGELOG.md index 24bcaa4..31916d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p - Fix `Navigator` to avoid animating expand/collapse transition on the initial mount. - Fix unintended scroll instead of canvas zoom on mouse wheel event over non-scrollable elements rendered with `CanvasPlaceAt` (when `zoomOptions.requireCtrl` is `false`): * Allow to explicitly prevent zoom on mouse wheel over a non-scrollable element with `data-reactodia-prevent-zoom` attribute. +- Fix accidentally moving elements or links with touch controls when panning which were also not recorded in the command history. #### ⏱ Performance - Fix canvas panning optimization not being applied due to incorrect `z-index` value. @@ -34,6 +35,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p * Prevent toggle buttons on `WorkspaceLayoutRow` children from being partially hidden when corresponding item is collapsed. - Export `TranslationProvider` and `DefaultTranslation` to be able to use `useTranslation()` outside the workspace component: * Remove deprecated `Translation.formatIri()` method (use `DataLocaleProvider.formatIri()` instead). +- Extend `CommandBatch.discard()` to accept `revert` option to be able to revert the batch without storing it first. - Always display ungroup buttons on `StandardGroup` when the element is single-selected. - Allow to configure `SearchResults` utility component with `isItemDisabled` and `multiSelection` props: * Remove `singleSelectOnClick` mode from `SearchResults` as it mostly superseded by `multiSelection`. diff --git a/src/diagram/canvasArea.tsx b/src/diagram/canvasArea.tsx index 21cd77d..63c60bd 100644 --- a/src/diagram/canvasArea.tsx +++ b/src/diagram/canvasArea.tsx @@ -370,7 +370,11 @@ class CanvasController implements CanvasApi { onPointerMove: e => this.onPointerMove(e, state), onPointerUp: (e, options) => this.onPointerUp(e, options, state), onPointerCancel: () => { - state.batch.discard(); + const restore = restoreGeometry.filterOutUnchanged(); + if (restore.hasChanges()) { + batch.history.registerToUndo(restore); + } + batch.discard({ revert: true }); }, }; }; diff --git a/src/diagram/history.ts b/src/diagram/history.ts index f2fed28..8b95c09 100644 --- a/src/diagram/history.ts +++ b/src/diagram/history.ts @@ -236,12 +236,20 @@ export interface CommandBatch { */ store(): void; /** - * Discards the batch, throwing away all nested commands, so they cannot be undone. + * Discards the batch, throwing away or reverting all nested commands. * - * This is useful when performing state changes on temporary items to - * avoid being able to revert it. + * Omitting `revert` or setting it to `false` is useful when performing transient state + * changes on temporary items to avoid being able to revert it. */ - discard(): void; + discard(options?: { + /** + * When set to `true` reverts all nested commands in the batch instead of + * throwing them away. + * + * @default false + */ + revert?: boolean; + }): void; } interface InMemoryBatch extends CommandBatch { @@ -325,13 +333,16 @@ export class InMemoryHistory implements CommandHistory { history: this, store: () => { if (!this.batches.includes(batch)) { - console.warn('Failed to find batch to store (already stored or discarded?)', batch); + console.warn( + 'Reactodia: failed to find batch to store (already stored or discarded?)', + batch + ); return; } while (this.batches.length > 0) { const other = this.batches.pop()!; if (other !== batch) { - console.warn('Storing other unclosed batch on top', other); + console.warn('Reactodia: storing other unclosed batch on top', other); } if (other._inverses.length > 0) { const commands = [...other._inverses].reverse(); @@ -342,15 +353,23 @@ export class InMemoryHistory implements CommandHistory { } } }, - discard: () => { + discard: (options) => { if (!this.batches.includes(batch)) { - console.warn('Failed to find batch to store (already stored or discarded?)'); + console.warn( + 'Reactodia: failed to find batch to discard (already stored or discarded?)', + batch + ); return; } while (this.batches.length > 0) { - const other = this.batches.pop(); + const other = this.batches.pop()!; if (other !== batch) { - console.warn('Discarding other unclosed batch on top', other); + console.warn('Reactodia: discarding other unclosed batch on top', other); + } + if (options?.revert && other._inverses.length > 0) { + const commands = [...other._inverses].reverse(); + const compound = Command.compound(batch._title, commands); + compound.invoke(); } if (other === batch) { break;