clean up undo/redo implementation
This commit is contained in:
parent
cd54937573
commit
2419880af3
4 changed files with 66 additions and 154 deletions
|
|
@ -1,47 +0,0 @@
|
||||||
# Svelte + TS + Vite
|
|
||||||
|
|
||||||
This template should help get you started developing with Svelte and TypeScript in Vite.
|
|
||||||
|
|
||||||
## Recommended IDE Setup
|
|
||||||
|
|
||||||
[VS Code](https://code.visualstudio.com/) + [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode).
|
|
||||||
|
|
||||||
## Need an official Svelte framework?
|
|
||||||
|
|
||||||
Check out [SvelteKit](https://github.com/sveltejs/kit#readme), which is also powered by Vite. Deploy anywhere with its serverless-first approach and adapt to various platforms, with out of the box support for TypeScript, SCSS, and Less, and easily-added support for mdsvex, GraphQL, PostCSS, Tailwind CSS, and more.
|
|
||||||
|
|
||||||
## Technical considerations
|
|
||||||
|
|
||||||
**Why use this over SvelteKit?**
|
|
||||||
|
|
||||||
- It brings its own routing solution which might not be preferable for some users.
|
|
||||||
- It is first and foremost a framework that just happens to use Vite under the hood, not a Vite app.
|
|
||||||
|
|
||||||
This template contains as little as possible to get started with Vite + TypeScript + Svelte, while taking into account the developer experience with regards to HMR and intellisense. It demonstrates capabilities on par with the other `create-vite` templates and is a good starting point for beginners dipping their toes into a Vite + Svelte project.
|
|
||||||
|
|
||||||
Should you later need the extended capabilities and extensibility provided by SvelteKit, the template has been structured similarly to SvelteKit so that it is easy to migrate.
|
|
||||||
|
|
||||||
**Why `global.d.ts` instead of `compilerOptions.types` inside `jsconfig.json` or `tsconfig.json`?**
|
|
||||||
|
|
||||||
Setting `compilerOptions.types` shuts out all other types not explicitly listed in the configuration. Using triple-slash references keeps the default TypeScript setting of accepting type information from the entire workspace, while also adding `svelte` and `vite/client` type information.
|
|
||||||
|
|
||||||
**Why include `.vscode/extensions.json`?**
|
|
||||||
|
|
||||||
Other templates indirectly recommend extensions via the README, but this file allows VS Code to prompt the user to install the recommended extension upon opening the project.
|
|
||||||
|
|
||||||
**Why enable `allowJs` in the TS template?**
|
|
||||||
|
|
||||||
While `allowJs: false` would indeed prevent the use of `.js` files in the project, it does not prevent the use of JavaScript syntax in `.svelte` files. In addition, it would force `checkJs: false`, bringing the worst of both worlds: not being able to guarantee the entire codebase is TypeScript, and also having worse typechecking for the existing JavaScript. In addition, there are valid use cases in which a mixed codebase may be relevant.
|
|
||||||
|
|
||||||
**Why is HMR not preserving my local component state?**
|
|
||||||
|
|
||||||
HMR state preservation comes with a number of gotchas! It has been disabled by default in both `svelte-hmr` and `@sveltejs/vite-plugin-svelte` due to its often surprising behavior. You can read the details [here](https://github.com/rixo/svelte-hmr#svelte-hmr).
|
|
||||||
|
|
||||||
If you have state that's important to retain within a component, consider creating an external store which would not be replaced by HMR.
|
|
||||||
|
|
||||||
```ts
|
|
||||||
// store.ts
|
|
||||||
// An extremely simple external store
|
|
||||||
import { writable } from 'svelte/store'
|
|
||||||
export default writable(0)
|
|
||||||
```
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.5 KiB |
|
|
@ -6,12 +6,13 @@ commit queue, datum editor, and field headers.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { AsyncTaskQueue, withRetry } from "attq";
|
||||||
|
|
||||||
import { type Datum, parse_datum_from_text } from "../datum.svelte";
|
import { type Datum, parse_datum_from_text } from "../datum.svelte";
|
||||||
import DatumEditor from "../datum-editor.svelte";
|
import DatumEditor from "../datum-editor.svelte";
|
||||||
import { BLUR_DEBOUNCE_MS } from "../datum-editor-common.svelte";
|
import { BLUR_DEBOUNCE_MS } from "../datum-editor-common.svelte";
|
||||||
import { type Row, type FieldInfo } from "../field.svelte";
|
import { type Row, type FieldInfo } from "../field.svelte";
|
||||||
import { get_empty_datum_for } from "../presentation.svelte";
|
import { get_empty_datum_for } from "../presentation.svelte";
|
||||||
import { UndoStack, type Undoable } from "./undo-stack.svelte";
|
|
||||||
import {
|
import {
|
||||||
type Coords,
|
type Coords,
|
||||||
coords_eq,
|
coords_eq,
|
||||||
|
|
@ -21,7 +22,7 @@ commit queue, datum editor, and field headers.
|
||||||
import FieldAdder from "./field-adder.svelte";
|
import FieldAdder from "./field-adder.svelte";
|
||||||
import FieldHeader from "./field-header.svelte";
|
import FieldHeader from "./field-header.svelte";
|
||||||
import TableCell from "./table-cell.svelte";
|
import TableCell from "./table-cell.svelte";
|
||||||
import { AsyncTaskQueue, withRetry } from "attq";
|
import { invert_diff, type Undoable, UndoStack } from "./undo-stack.svelte";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
columns: {
|
columns: {
|
||||||
|
|
@ -42,14 +43,9 @@ commit queue, datum editor, and field headers.
|
||||||
total_count,
|
total_count,
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
type Selection = {
|
|
||||||
coords: Coords;
|
|
||||||
original_value: Datum;
|
|
||||||
};
|
|
||||||
|
|
||||||
type Delta = Undoable<Coords, Datum>;
|
type Delta = Undoable<Coords, Datum>;
|
||||||
|
|
||||||
let selections = $state<Selection[]>([]);
|
let selections = $state<Delta[]>([]);
|
||||||
let editor_value = $state<Datum | undefined>(undefined);
|
let editor_value = $state<Datum | undefined>(undefined);
|
||||||
let datum_editor = $state<DatumEditor | undefined>();
|
let datum_editor = $state<DatumEditor | undefined>();
|
||||||
let focus_cursor = $state<(() => unknown) | undefined>();
|
let focus_cursor = $state<(() => unknown) | undefined>();
|
||||||
|
|
@ -95,15 +91,7 @@ commit queue, datum editor, and field headers.
|
||||||
// the UI and as replayed to the server side.
|
// the UI and as replayed to the server side.
|
||||||
const undo_stack = new UndoStack<Delta>({
|
const undo_stack = new UndoStack<Delta>({
|
||||||
apply_diff: (diff) => {
|
apply_diff: (diff) => {
|
||||||
for (const {
|
set_cell_values(diff);
|
||||||
loc: { region, row_idx, field_idx },
|
|
||||||
value_updated,
|
|
||||||
} of diff) {
|
|
||||||
// TODO: Does reactivity work with a ternary on the lhs?
|
|
||||||
(region === "main" ? rows_main : rows_inserter)[row_idx].data[
|
|
||||||
field_idx
|
|
||||||
] = value_updated;
|
|
||||||
}
|
|
||||||
upload_queue.push(diff);
|
upload_queue.push(diff);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -202,6 +190,16 @@ commit queue, datum editor, and field headers.
|
||||||
|
|
||||||
// -------- Updates and Effects -------- //
|
// -------- Updates and Effects -------- //
|
||||||
|
|
||||||
|
function set_cell_values(diff: Delta[]) {
|
||||||
|
for (const {
|
||||||
|
loc: { region, row_idx, field_idx },
|
||||||
|
value_updated,
|
||||||
|
} of diff) {
|
||||||
|
(region === "main" ? rows_main : rows_inserter)[row_idx].data[field_idx] =
|
||||||
|
value_updated;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function set_selections(arr: Coords[]) {
|
function set_selections(arr: Coords[]) {
|
||||||
selections = arr.map((coords) => {
|
selections = arr.map((coords) => {
|
||||||
let cell_data: Datum | undefined;
|
let cell_data: Datum | undefined;
|
||||||
|
|
@ -212,7 +210,11 @@ commit queue, datum editor, and field headers.
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`invalid region: ${coords.region}`);
|
throw new Error(`invalid region: ${coords.region}`);
|
||||||
}
|
}
|
||||||
return { coords, original_value: cell_data! };
|
return {
|
||||||
|
loc: coords,
|
||||||
|
value_initial: cell_data!,
|
||||||
|
value_updated: cell_data!,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
if (arr.length === 1) {
|
if (arr.length === 1) {
|
||||||
const [coords] = arr;
|
const [coords] = arr;
|
||||||
|
|
@ -241,23 +243,22 @@ commit queue, datum editor, and field headers.
|
||||||
if (
|
if (
|
||||||
additive &&
|
additive &&
|
||||||
first_selection !== undefined &&
|
first_selection !== undefined &&
|
||||||
!coords_eq(new_cursor, first_selection.coords)
|
!coords_eq(new_cursor, first_selection.loc)
|
||||||
) {
|
) {
|
||||||
// By convention, we keep the first selected cell at the end of the
|
// By convention, we keep the first selected cell at the end of the
|
||||||
// selections array, and the current cursor at the beginning. Everything
|
// selections array, and the current cursor at the beginning. Everything
|
||||||
// in the bounded box should be populated in between.
|
// in the bounded box should be populated in between.
|
||||||
set_selections([
|
set_selections([
|
||||||
new_cursor,
|
new_cursor,
|
||||||
...get_box(first_selection.coords, new_cursor, {
|
...get_box(first_selection.loc, new_cursor, {
|
||||||
n_fields: fields.length,
|
n_fields: fields.length,
|
||||||
n_rows_main: rows_main.length,
|
n_rows_main: rows_main.length,
|
||||||
n_rows_inserter: rows_inserter.length,
|
n_rows_inserter: rows_inserter.length,
|
||||||
}).filter(
|
}).filter(
|
||||||
(sel) =>
|
(sel) =>
|
||||||
!coords_eq(sel, new_cursor) &&
|
!coords_eq(sel, new_cursor) && !coords_eq(sel, first_selection.loc),
|
||||||
!coords_eq(sel, first_selection.coords),
|
|
||||||
),
|
),
|
||||||
first_selection.coords,
|
first_selection.loc,
|
||||||
]);
|
]);
|
||||||
} else {
|
} else {
|
||||||
set_selections([new_cursor]);
|
set_selections([new_cursor]);
|
||||||
|
|
@ -265,28 +266,16 @@ commit queue, datum editor, and field headers.
|
||||||
}
|
}
|
||||||
|
|
||||||
function try_sync_edit_to_cells() {
|
function try_sync_edit_to_cells() {
|
||||||
if (selections.length === 0) {
|
|
||||||
console.warn("preconditions for try_sync_edit_to_cells() not met");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Copy value locally so that it can be used intuitively in closures.
|
// Copy value locally so that it can be used intuitively in closures.
|
||||||
const editor_value_scoped = editor_value;
|
const editor_value_scoped = editor_value;
|
||||||
if (editor_value_scoped === undefined) {
|
if (editor_value_scoped !== undefined) {
|
||||||
return;
|
// Editor state represents a valid cell value.
|
||||||
}
|
selections = selections.map((sel) => ({
|
||||||
for (const sel of selections) {
|
...sel,
|
||||||
// TODO: Refactor into `set_cell_values` function or similar to avoid
|
value_updated: editor_value_scoped,
|
||||||
// duplicating work with `apply_diffs()` callback of `undo_stack`.
|
}));
|
||||||
if (sel.coords.region === "main") {
|
|
||||||
rows_main[sel.coords.row_idx].data[sel.coords.field_idx] =
|
|
||||||
editor_value_scoped;
|
|
||||||
} else if (sel.coords.region === "inserter") {
|
|
||||||
rows_inserter[sel.coords.row_idx].data[sel.coords.field_idx] =
|
|
||||||
editor_value_scoped;
|
|
||||||
} else {
|
|
||||||
throw new Error("Unknown region");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
set_cell_values(selections);
|
||||||
}
|
}
|
||||||
|
|
||||||
function try_queue_delta() {
|
function try_queue_delta() {
|
||||||
|
|
@ -301,9 +290,9 @@ commit queue, datum editor, and field headers.
|
||||||
} else {
|
} else {
|
||||||
if (selections.length > 0) {
|
if (selections.length > 0) {
|
||||||
undo_stack.push(
|
undo_stack.push(
|
||||||
selections.map(({ coords, original_value }) => ({
|
selections.map(({ loc, value_initial }) => ({
|
||||||
loc: coords,
|
loc,
|
||||||
value_initial: original_value,
|
value_initial,
|
||||||
value_updated: editor_value_scoped,
|
value_updated: editor_value_scoped,
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
|
@ -316,17 +305,9 @@ commit queue, datum editor, and field headers.
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancel_edit() {
|
function cancel_edit() {
|
||||||
selections.forEach(({ coords, original_value }) => {
|
set_cell_values(invert_diff(selections));
|
||||||
if (coords.region === "main") {
|
|
||||||
rows_main[coords.row_idx].data[coords.field_idx] = original_value;
|
|
||||||
} else if (coords.region === "inserter") {
|
|
||||||
rows_inserter[coords.row_idx].data[coords.field_idx] = original_value;
|
|
||||||
} else {
|
|
||||||
throw new Error("Unknown region");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// Reset editor input value
|
// Reset editor input value
|
||||||
set_selections(selections.map(({ coords }) => coords));
|
set_selections(selections.map(({ loc }) => loc));
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------- Event Handlers -------- //
|
// -------- Event Handlers -------- //
|
||||||
|
|
@ -340,16 +321,11 @@ commit queue, datum editor, and field headers.
|
||||||
const row_offset =
|
const row_offset =
|
||||||
arrow_direction === "Down" ? 1 : arrow_direction === "Up" ? -1 : 0;
|
arrow_direction === "Down" ? 1 : arrow_direction === "Up" ? -1 : 0;
|
||||||
const cursor = selections[0];
|
const cursor = selections[0];
|
||||||
const new_cursor = offset_coords(
|
const new_cursor = offset_coords(cursor.loc, row_offset, field_offset, {
|
||||||
cursor.coords,
|
|
||||||
row_offset,
|
|
||||||
field_offset,
|
|
||||||
{
|
|
||||||
n_fields: fields.length,
|
n_fields: fields.length,
|
||||||
n_rows_main: rows_main.length,
|
n_rows_main: rows_main.length,
|
||||||
n_rows_inserter: rows_inserter.length,
|
n_rows_inserter: rows_inserter.length,
|
||||||
},
|
});
|
||||||
);
|
|
||||||
if (ev.shiftKey) {
|
if (ev.shiftKey) {
|
||||||
move_cursor(new_cursor, { additive: true });
|
move_cursor(new_cursor, { additive: true });
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -370,19 +346,19 @@ commit queue, datum editor, and field headers.
|
||||||
const sel = selections[0];
|
const sel = selections[0];
|
||||||
if (sel) {
|
if (sel) {
|
||||||
editor_value = get_empty_datum_for(
|
editor_value = get_empty_datum_for(
|
||||||
fields[sel.coords.field_idx].field.presentation,
|
fields[sel.loc.field_idx].field.presentation,
|
||||||
);
|
);
|
||||||
datum_editor?.focus();
|
datum_editor?.focus();
|
||||||
try_sync_edit_to_cells();
|
try_sync_edit_to_cells();
|
||||||
}
|
}
|
||||||
} else if (ev.key === "Enter") {
|
} else if (ev.key === "Enter") {
|
||||||
if (ev.shiftKey) {
|
if (ev.shiftKey) {
|
||||||
if (selections[0]?.coords.region === "main") {
|
if (selections[0]?.loc.region === "main") {
|
||||||
set_selections([
|
set_selections([
|
||||||
{
|
{
|
||||||
region: "inserter",
|
region: "inserter",
|
||||||
row_idx: 0,
|
row_idx: 0,
|
||||||
field_idx: selections[0]?.coords.field_idx ?? 0,
|
field_idx: selections[0]?.loc.field_idx ?? 0,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -430,17 +406,17 @@ commit queue, datum editor, and field headers.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const top_left: Coords = {
|
const top_left: Coords = {
|
||||||
region: selections.some(({ coords: { region } }) => region === "main")
|
region: selections.some(({ loc: { region } }) => region === "main")
|
||||||
? "main"
|
? "main"
|
||||||
: "inserter",
|
: "inserter",
|
||||||
field_idx: Math.min(
|
field_idx: Math.min(
|
||||||
...selections.map(({ coords: { field_idx } }) => field_idx),
|
...selections.map(({ loc: { field_idx } }) => field_idx),
|
||||||
),
|
),
|
||||||
row_idx: Math.min(
|
row_idx: Math.min(
|
||||||
...(selections.some(({ coords: { region } }) => region === "main")
|
...(selections.some(({ loc: { region } }) => region === "main")
|
||||||
? selections.filter(({ coords: { region } }) => region === "main")
|
? selections.filter(({ loc: { region } }) => region === "main")
|
||||||
: selections
|
: selections
|
||||||
).map(({ coords: { row_idx } }) => row_idx),
|
).map(({ loc: { row_idx } }) => row_idx),
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
const fields_to_right = fields.slice(top_left.field_idx);
|
const fields_to_right = fields.slice(top_left.field_idx);
|
||||||
|
|
@ -490,22 +466,6 @@ commit queue, datum editor, and field headers.
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
parsed_tsv.forEach((row, i) =>
|
|
||||||
row.map((value, j) => {
|
|
||||||
const coords = offset_coords(top_left, i, j, {
|
|
||||||
n_fields: fields.length,
|
|
||||||
n_rows_main: rows_main.length,
|
|
||||||
n_rows_inserter: rows_inserter.length,
|
|
||||||
});
|
|
||||||
if (coords.region === "main") {
|
|
||||||
rows_main[coords.row_idx].data[coords.field_idx] = value;
|
|
||||||
} else if (coords.region === "inserter") {
|
|
||||||
rows_inserter[coords.row_idx].data[coords.field_idx] = value;
|
|
||||||
} else {
|
|
||||||
throw new Error("Unknown region");
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
// TODO: pasting into multiple selections
|
// TODO: pasting into multiple selections
|
||||||
}
|
}
|
||||||
|
|
@ -537,7 +497,7 @@ commit queue, datum editor, and field headers.
|
||||||
<TableCell
|
<TableCell
|
||||||
coords={{ region, row_idx, field_idx }}
|
coords={{ region, row_idx, field_idx }}
|
||||||
cursor={selections.length !== 0 &&
|
cursor={selections.length !== 0 &&
|
||||||
coords_eq(selections[0].coords, {
|
coords_eq(selections[0].loc, {
|
||||||
region,
|
region,
|
||||||
row_idx,
|
row_idx,
|
||||||
field_idx,
|
field_idx,
|
||||||
|
|
@ -552,8 +512,8 @@ commit queue, datum editor, and field headers.
|
||||||
onpaste={handle_cell_paste}
|
onpaste={handle_cell_paste}
|
||||||
selected={selections.some(
|
selected={selections.some(
|
||||||
(sel) =>
|
(sel) =>
|
||||||
sel.coords.region === region &&
|
sel.loc.region === region &&
|
||||||
coords_eq(sel.coords, { region, row_idx, field_idx }),
|
coords_eq(sel.loc, { region, row_idx, field_idx }),
|
||||||
)}
|
)}
|
||||||
table_region={region}
|
table_region={region}
|
||||||
value={row.data[field_idx]}
|
value={row.data[field_idx]}
|
||||||
|
|
@ -613,8 +573,8 @@ commit queue, datum editor, and field headers.
|
||||||
set_selections([
|
set_selections([
|
||||||
coords,
|
coords,
|
||||||
...selections
|
...selections
|
||||||
.filter((sel) => !coords_eq(sel.coords, coords))
|
.filter(({ loc }) => !coords_eq(loc, coords))
|
||||||
.map((sel) => sel.coords),
|
.map(({ loc }) => loc),
|
||||||
]);
|
]);
|
||||||
} else if (ev.shiftKey) {
|
} else if (ev.shiftKey) {
|
||||||
move_cursor(coords, { additive: true });
|
move_cursor(coords, { additive: true });
|
||||||
|
|
@ -650,8 +610,8 @@ commit queue, datum editor, and field headers.
|
||||||
set_selections([
|
set_selections([
|
||||||
coords,
|
coords,
|
||||||
...selections
|
...selections
|
||||||
.filter((sel) => !coords_eq(sel.coords, coords))
|
.filter(({ loc }) => !coords_eq(loc, coords))
|
||||||
.map((sel) => sel.coords),
|
.map(({ loc }) => loc),
|
||||||
]);
|
]);
|
||||||
} else if (ev.shiftKey) {
|
} else if (ev.shiftKey) {
|
||||||
move_cursor(coords, { additive: true });
|
move_cursor(coords, { additive: true });
|
||||||
|
|
@ -688,17 +648,15 @@ commit queue, datum editor, and field headers.
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="datum-editor">
|
<div class="datum-editor">
|
||||||
{#if selections.length !== 0 && selections.every(({ coords: { field_idx } }) => field_idx === selections[0]?.coords.field_idx)}
|
{#if selections.length !== 0 && selections.every(({ loc: { field_idx } }) => field_idx === selections[0]?.loc.field_idx)}
|
||||||
<DatumEditor
|
<DatumEditor
|
||||||
bind:this={datum_editor}
|
bind:this={datum_editor}
|
||||||
bind:value={editor_value}
|
bind:value={editor_value}
|
||||||
current_presentation={fields[selections[0].coords.field_idx].field
|
current_presentation={fields[selections[0].loc.field_idx].field
|
||||||
.presentation}
|
.presentation}
|
||||||
on_blur={() => try_queue_delta()}
|
on_blur={try_queue_delta}
|
||||||
on_cancel_edit={cancel_edit}
|
on_cancel_edit={cancel_edit}
|
||||||
on_change={() => {
|
on_change={try_sync_edit_to_cells}
|
||||||
try_sync_edit_to_cells();
|
|
||||||
}}
|
|
||||||
on_restore_focus={handle_restore_focus}
|
on_restore_focus={handle_restore_focus}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ export class UndoStack<T extends Undoable<unknown, unknown>> {
|
||||||
|
|
||||||
// Call `_apply_diff()` after shifting cursor, in case it recursively
|
// Call `_apply_diff()` after shifting cursor, in case it recursively
|
||||||
// mutates this UndoStack.
|
// mutates this UndoStack.
|
||||||
this._apply_diff(this._diffs[this._cursor + 1].map(invert));
|
this._apply_diff(invert_diff(this._diffs[this._cursor + 1]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -73,10 +73,12 @@ export class UndoStack<T extends Undoable<unknown, unknown>> {
|
||||||
/**
|
/**
|
||||||
* Returns a copy of the parameter with initial and updated values swapped.
|
* Returns a copy of the parameter with initial and updated values swapped.
|
||||||
*/
|
*/
|
||||||
function invert<T extends Undoable<unknown, unknown>>(undoable: T): T {
|
export function invert_diff<T extends Undoable<unknown, unknown>>(
|
||||||
return {
|
diff: T[],
|
||||||
|
): T[] {
|
||||||
|
return diff.map((undoable) => ({
|
||||||
loc: undoable.loc,
|
loc: undoable.loc,
|
||||||
value_initial: undoable.value_updated,
|
value_initial: undoable.value_updated,
|
||||||
value_updated: undoable.value_initial,
|
value_updated: undoable.value_initial,
|
||||||
} as T;
|
} as T));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue