Skip to content

Instantly share code, notes, and snippets.

@csenio
Created January 1, 2021 16:43
Show Gist options
  • Save csenio/246b453db80a6755f2d1ea8b15ba3b88 to your computer and use it in GitHub Desktop.
Save csenio/246b453db80a6755f2d1ea8b15ba3b88 to your computer and use it in GitHub Desktop.
// full explanation here: https://jsco.dev/blog/undo-across-multiple-stores-in-svelte
/* Use like this:
* import undoable, { undoTracker } from "./undoable.js";
* const myStore = undoable(INITIAL_VALUE)
* if(undoTracker.can_undo) undoTracker.undo()
* if(undoTracker.can_redo) undoTracker.redo()
*/
import {writable, derived, get} from "svelte/store"
import {tick} from "svelte"
function createUndoTracker() {
const undoStore = writable([])
const redoStore = writable([])
let redoStack = []
redoStore.subscribe((val) => (redoStack = val))
let undoStack = []
undoStore.subscribe((val) => (undoStack = val))
function redo() {
if (redoStack.length === 0) return
const [lastUpdate, ...restUpdates] = redoStack
// update origin store
lastUpdate.store.set(lastUpdate.to)
// remove from redo store
redoStore.set(restUpdates)
// add back to undo store
undoStore.update((arr) => [lastUpdate, ...arr])
}
function undo() {
if (undoStack.length === 0) return
const [lastUpdate, ...restUpdates] = undoStack
// update origin store
lastUpdate.store.set(lastUpdate.from)
// remove from undo store
undoStore.set(restUpdates)
// add to redo store
redoStore.update((arr) => [lastUpdate, ...arr])
}
let currentPatch = false
function track({from, to, store, synchronous}) {
if (synchronous) {
undoStore.update((stack) => [{from, to, store}, ...stack])
} else if (!currentPatch) {
currentPatch = {
from,
timerId: setTimeout(() => {
undoStore.update((stack) => [
{from: currentPatch.from, to, store},
...stack,
])
currentPatch = false
}, 200),
}
} else {
// patch updates that happen very frequently together
clearTimeout(currentPatch.timerId)
currentPatch.timerId = setTimeout(() => {
console.log("track", {from: currentPatch.from, to, store})
undoStore.update((stack) => [
{from: currentPatch.from, to, store},
...stack,
])
currentPatch = false
}, 200)
}
}
const can_undo = derived(undoStore, (arr) => arr.length > 0)
const can_redo = derived(redoStore, (arr) => arr.length > 0)
return {
undoStore,
redoStore,
track,
undo,
can_undo,
redo,
can_redo,
}
}
export const undoTracker = createUndoTracker()
export default function undoable(initialData, options) {
const store = writable(initialData)
const {subscribe, set, update} = store
function trackUndoState(previous) {
tick().then(() =>
undoTracker.track({
from: previous,
to: get(store),
store,
synchronous: options?.noDebounce,
}),
)
}
function _set(newValue) {
trackUndoState(get(store))
set(newValue)
}
function _update(updater) {
trackUndoState(get(store))
update(updater)
}
return {
subscribe,
set: _set,
update: _update,
reset: () => set(0),
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment