Last active
October 14, 2024 01:43
-
-
Save 1j01/bd2329547904b97abc52fd5e76b008d8 to your computer and use it in GitHub Desktop.
Revisions
-
1j01 revised this gist
Jun 10, 2019 . 1 changed file with 34 additions and 24 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,6 +1,28 @@ // first setup our stacks of history states // (const refers to the reference to the arrays, but they arrays themselves are mutable) const undos = []; const redos = []; // undo and redo are symmetrical operations, so they *could* be factored to use an "undoOrRedo" / "stepHistory" function that takes two stacks as arguments, but it might be clearer as two functions const undo = () => { if (undos.length < 1) { return false; } redos.push(getState()); setState(undos.pop()); return true; }; const redo = () => { if (redos.length < 1) { return false; } undos.push(getState()); setState(redos.pop()); return true; }; // usage example: undoable(()=> { state.width = 100; }); // (this also supports calling undoable with no action function, before // continuously changing the state (e.g. adding points to a brush stroke), @@ -25,26 +47,6 @@ const undoable = (synchronousAction) => { return true; }; // --- now interface with the application --- let state = { @@ -72,8 +74,6 @@ const onStateChanged = () => { /* // Note: this doesn't handle multiple tabs / instances of the app. // Only the last updated one will "win", and data in any other instances will be lost. try { localStorage.appState = getState(); } catch (error) { @@ -124,10 +124,20 @@ redo(); console.log(state); // TODO: hook up Ctrl+Z to undo(), and both Ctrl+Y and Ctrl+Shift+Z to redo() // (Ctrl+Y is conventional in Windows, and Ctrl+Shift+Z is very common across platforms because it's related to Ctrl+Z and easy to press with one hand) // (For macOS it would be Cmd+Z and Cmd+Shift+Z) // Recommended: also create toolbar buttons with tooltips that show the keyboard shortcut // TODO: don't destroy history as is the common practice, but instead provide nonlinear history - // keep a tree of states and show this tree to the user when they attempt to redo after undoing and doing something other than undo/redo; // that way they can at least get back to any state, if not merge states after diverging, which could be complicated; // also provide a way to show the tree at any time, perhaps ctrl+shift+y // TODO: include undo/redo history in localStorage, and give the user a (clear) way to clear it // TODO: for autosave, to avoid conflicts with multiple instances of the app, have "sessions" // and add UI for managing sessions (e.g. in jspaint, File > Manage Storage). // (You could maybe, on app start, look for existing sessions, // and try to communicate with other tabs (with a SharedWorker? BroadcastChannel API?) // to see what sessions are open (with some timeout on messaging response time), // and present UI to "recover" sessions that are not open.) -
1j01 revised this gist
Jun 7, 2019 . 1 changed file with 35 additions and 10 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -12,10 +12,7 @@ const undoable = (synchronousAction) => { // you may also want to mark the document as unsaved here // DESTROY any redos - this is the common practice, but let's examine this assumption later redos.length = 0; undos.push(getState()); @@ -28,6 +25,8 @@ const undoable = (synchronousAction) => { return true; }; // undo and redo are symmetrical, so they *could* be factored into an undoOrRedo / stepHistory function (that takes two stacks as arguments), but this might be clearer as two functions const undo = () => { if (undos.length < 1) { return false; } @@ -54,6 +53,7 @@ let state = { // and the user can be told when old states aren't supported // for an example of this pattern, see https://github.com/1j01/mopaint/blob/2db35857fbbb76980ce1229dadef531bf5147fc6/src/components/App.js#L60 // (that one TODO comment is accidentally left in there, it's done with `nounPhraseThingToLoad`) // format: "my-app", // formatVersion: 1, width: 640, @@ -62,14 +62,18 @@ let state = { }; const onStateChanged = () => { // could update any derived state here to ensure consistency when saving // or better yet, keep derived state out of `state`! // (derived state would be, for instance, in a traffic simulator, if the user places roads, // but trees are filled in automatically as decoration - // if the trees are a function of the roads, they are derived state) // you should probably save the user's data: /* // Note: this doesn't handle multiple tabs / instances of the app. // Only the last updated one will "win", and data in any other instances will be lost. // TODO: have "sessions" and add UI for managing them (e.g. in jspaint, File > Manage Storage). // (You could maybe, on app start, look for existing sessions, and try to communicate with other tabs to see what sessions are open (with some timeout on messaging response time), and present UI to "recover" sessions that are not open.) try { localStorage.appState = getState(); } catch (error) { @@ -80,6 +84,8 @@ const onStateChanged = () => { } */ // alternatively you could render in an animation loop (i.e. constantly), // or you could use a pattern like Observables to react to state changes render(); }; @@ -91,13 +97,14 @@ const setState = (stateJSON) => { state = JSON.parse(stateJSON); onStateChanged(); }; // (you could swap out JSON for a library like ARSON if you need cyclic references, etc.) const render = () => { // TODO (render based on state) }; /* // load from storage try { if (localStorage.appState) { setState(localStorage.appState); @@ -106,3 +113,21 @@ try { */ render(); // some example usage console.log(state); undoable(()=> { state.width = 100; }); console.log(state); undo(); console.log(state); redo(); console.log(state); // TODO: hook up Ctrl+Z to undo(), and both Ctrl+Y and Ctrl+Shift+Z to redo() // (Ctrl+Y is a convention in Windows, and Ctrl+Shift+Z is very common because it's related to Ctrl+Z and easy to press with one hand) // also create toolbar buttons for these actions, with tooltips that show the keyboard shortcut // TODO: don't destroy history as is the common practice, but instead provide nonlinear history - // keep a tree of states and show this tree to the user when they attempt to redo after undoing and doing something other than undo/redo; // that way they can at least get back to any state, if not merge states after diverging, which could be complicated; // also provide a way to show the tree at any time, perhaps ctrl+shift+y -
1j01 created this gist
Jun 2, 2019 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,108 @@ const undos = []; const redos = []; // usage example: undoable(()=> { state.width = 100; }); // (this also supports calling undoable with no action function, before // continuously changing the state (e.g. adding points to a brush stroke), // but this is not recommended; it would be better to, for instance, // have a separate state for a brush stroke in progress, // and add it to the main state as an undoable step when it's finished; // this would also let you cancel the operation simply) const undoable = (synchronousAction) => { // you may also want to mark the document as unsaved here // TODO: don't destroy history as is the common practice, but instead provide nonlinear history - // keep a tree of states and show this tree to the user when they attempt to redo after undoing and doing something other than undo/redo; // that way they can at least get back to any state, if not merge states after diverging, which could be complicated; // also provide a way to show the tree at any time, perhaps ctrl+shift+y redos.length = 0; undos.push(getState()); if (synchronousAction) { synchronousAction(); onStateChanged(); } return true; }; const undo = () => { if (undos.length < 1) { return false; } redos.push(getState()); setState(undos.pop()); return true; }; const redo = () => { if (redos.length < 1) { return false; } undos.push(getState()); setState(redos.pop()); return true; }; // --- now interface with the application --- let state = { // if you're going to save files based on the serialized state OR persist it in localStorage, // you should include a version number, so old states can be upgraded, // and the user can be told when old states aren't supported // for an example of this pattern, see https://github.com/1j01/mopaint/blob/2db35857fbbb76980ce1229dadef531bf5147fc6/src/components/App.js#L60 // (that one TODO comment is accidentally left in there, it's done with `nounPhraseThingToLoad`) // formatVersion: 1, width: 640, height: 480, strokes: [] }; const onStateChanged = () => { // could update any computed (derived) state here to ensure consistency when saving // or better yet, keep computed state out of `state`! // (derived state would be, for instance, if your app is a traffic simulator, and the user places roads, // but buildings and trees are filled in automatically as decoration, // if the buildings and trees are a function of the road structure, they are derived state) // you should probably save the user's data: /* try { localStorage.appState = getState(); } catch (error) { // show a warning here that data isn't saved // but don't use alert() or anything that obtrusive // (the user should still be able to use the tool with site data turned off in their browser, // but just be made aware that their data isn't safe (and why)) } */ render(); }; // serialize the state const getState = () => JSON.stringify(state); // deserialize and apply a state const setState = (stateJSON) => { state = JSON.parse(stateJSON); onStateChanged(); }; const render = () => { // TODO (render based on state) }; /* // load from localStorage try { if (localStorage.appState) { setState(localStorage.appState); } } catch(err) { } */ render();