9df16a2cf698064f4bf1c1de86d6184ac1529425c0c95660006b1ca68e9b3c7115acf52ad5b65b5ce64f3869b937a331e245b365187e35473fe55310df6868 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  1. # Undo Manager
  2. Simple undo manager to provide undo and redo actions in JavaScript applications.
  3. - [Demos](#demos)
  4. - [Installation](#installation)
  5. - [Example](#example)
  6. - [Updating the UI](#updating-the-ui)
  7. - [Methods](#methods)
  8. - [add](#add)
  9. - [undo](#undo)
  10. - [redo](#redo)
  11. - [clear](#clear)
  12. - [setLimit](#setlimit)
  13. - [hasUndo](#hasundo)
  14. - [hasRedo](#hasredo)
  15. - [setCallback](#setcallback)
  16. - [getIndex](#getindex)
  17. - [getCommands](#getcommands)
  18. - [Use with CommonJS](#use-with-commonjs)
  19. - [Use with RequireJS](#use-with-requirejs)
  20. ## Demos
  21. * [CodeSandbox with RGB color slider](https://codesandbox.io/s/undo-manager-color-sliders-z4myoj)
  22. * [Undo Manager with canvas drawing](https://arthurclemens.github.io/JavaScript-Undo-Manager/)
  23. * [JSBin demo, also with canvas](http://jsbin.com/tidibi/edit?js,output)
  24. ## Installation
  25. ```
  26. npm install undo-manager
  27. ```
  28. ## Example
  29. Actions (typing a character, moving an object) are structured as command pairs: one command for destruction (undo) and one for creation (redo). Each pair is added to the undo stack:
  30. ```js
  31. const undoManager = new UndoManager();
  32. undoManager.add({
  33. undo: function() {
  34. // ...
  35. },
  36. redo: function() {
  37. // ...
  38. }
  39. });
  40. ```
  41. To make an action undoable, you'd add an undo/redo command pair to the undo manager:
  42. ```js
  43. const undoManager = new UndoManager();
  44. const people = {};
  45. function addPerson(id, name) {
  46. people[id] = name;
  47. };
  48. function removePerson(id) {
  49. delete people[id];
  50. };
  51. function createPerson(id, name) {
  52. // first creation
  53. addPerson(id, name);
  54. // make undoable
  55. undoManager.add({
  56. undo: () => removePerson(id),
  57. redo: () => addPerson(id, name)
  58. });
  59. }
  60. createPerson(101, "John");
  61. createPerson(102, "Mary");
  62. console.log(people); // logs: {101: "John", 102: "Mary"}
  63. undoManager.undo();
  64. console.log(people); // logs: {101: "John"}
  65. undoManager.undo();
  66. console.log(people); // logs: {}
  67. undoManager.redo();
  68. console.log(people); // logs: {101: "John"}
  69. ```
  70. ## Updating the UI
  71. TL;DR UI that relies on undo manager state - for example `hasUndo` and `hasRedo` - needs to be updated using the callback function provided with `setCallback`. This ensures that all internal state has been resolved before the UI is repainted.
  72. Let's say we have an update function that conditionally disables the undo and redo buttons:
  73. ```js
  74. function updateUI() {
  75. btn_undo.disabled = !undoManager.hasUndo();
  76. btn_redo.disabled = !undoManager.hasRedo();
  77. }
  78. ```
  79. You might be inclined to call the update in the undo/redo command pair:
  80. ```js
  81. // wrong approach, don't copy
  82. const undoManager = new UndoManager();
  83. const states = [];
  84. function updateState(newState) {
  85. states.push(newState);
  86. updateUI();
  87. undoManager.add({
  88. undo: function () {
  89. states.pop();
  90. updateUI(); // <= this will lead to inconsistent UI state
  91. },
  92. redo: function () {
  93. states.push(newState);
  94. updateUI(); // <= this will lead to inconsistent UI state
  95. }
  96. });
  97. }
  98. ```
  99. Instead, pass the update function to `setCallback`:
  100. ```js
  101. // recommended approach
  102. const undoManager = new UndoManager();
  103. undoManager.setCallback(updateUI);
  104. const states = [];
  105. function updateState(newState) {
  106. states.push(newState);
  107. updateUI();
  108. undoManager.add({
  109. undo: function () {
  110. states.pop();
  111. },
  112. redo: function () {
  113. states.push(newState);
  114. }
  115. });
  116. }
  117. ```
  118. ## Methods
  119. ### add
  120. Adds an undo/redo command pair to the stack.
  121. ```js
  122. function createPerson(id, name) {
  123. // first creation
  124. addPerson(id, name);
  125. // make undoable
  126. undoManager.add({
  127. undo: () => removePerson(id),
  128. redo: () => addPerson(id, name)
  129. });
  130. }
  131. ```
  132. Optionally add a `groupId` to identify related command pairs. Undo and redo actions will then be performed on all adjacent command pairs with that group id.
  133. ```js
  134. undoManager.add({
  135. groupId: 'auth',
  136. undo: () => removePerson(id),
  137. redo: () => addPerson(id, name)
  138. });
  139. ```
  140. ### undo
  141. Performs the undo action.
  142. ```js
  143. undoManager.undo();
  144. ```
  145. If a `groupId` was set, the undo action will be performed on all adjacent command pairs with that group id.
  146. ### redo
  147. Performs the redo action.
  148. ```js
  149. undoManager.redo();
  150. ```
  151. If a `groupId` was set, the redo action will be performed on all adjacent command pairs with that group id.
  152. ### clear
  153. Clears all stored states.
  154. ```js
  155. undoManager.clear();
  156. ```
  157. ### setLimit
  158. Set the maximum number of undo steps. Default: 0 (unlimited).
  159. ```js
  160. undoManager.setLimit(limit);
  161. ```
  162. ### hasUndo
  163. Tests if any undo actions exist.
  164. ```js
  165. const hasUndo = undoManager.hasUndo();
  166. ```
  167. ### hasRedo
  168. Tests if any redo actions exist.
  169. ```js
  170. const hasRedo = undoManager.hasRedo();
  171. ```
  172. ### setCallback
  173. Get notified on changes. Pass a function to be called on undo and redo actions.
  174. ```js
  175. undoManager.setCallback(myCallback);
  176. ```
  177. ### getIndex
  178. Returns the index of the actions list.
  179. ```js
  180. const index = undoManager.getIndex();
  181. ```
  182. ### getCommands
  183. Returns the list of queued commands, optionally filtered by group id.
  184. ```js
  185. const commands = undoManager.getCommands();
  186. const commands = undoManager.getCommands(groupId);
  187. ```
  188. ## Use with CommonJS
  189. ```bash
  190. npm install undo-manager
  191. ```
  192. ```js
  193. const UndoManager = require('undo-manager')
  194. ```
  195. If you only need a single instance of UndoManager throughout your application, it may be wise to create a module that exports a singleton:
  196. ```js
  197. // undoManager.js
  198. const undoManager = require('undo-manager'); // require the lib from node_modules
  199. let singleton = undefined;
  200. if (!singleton) {
  201. singleton = new undoManager();
  202. }
  203. module.exports = singleton;
  204. ```
  205. Then in your app:
  206. ```js
  207. // app.js
  208. const undoManager = require('undoManager');
  209. undoManager.add(...);
  210. undoManager.undo();
  211. ```
  212. ## Use with RequireJS
  213. If you are using RequireJS, you need to use the `shim` config parameter.
  214. Assuming `require.js` and `domReady.js` are located in `js/extern`, the `index.html` load call would be:
  215. ```html
  216. <script src="js/extern/require.js" data-main="js/demo"></script>
  217. ```
  218. And `demo.js` would look like this:
  219. ```js
  220. requirejs.config({
  221. baseUrl: "js",
  222. paths: {
  223. domReady: "extern/domReady",
  224. app: "../demo",
  225. undomanager: "../../js/undomanager",
  226. circledrawer: "circledrawer"
  227. },
  228. shim: {
  229. "undomanager": {
  230. exports: "UndoManager"
  231. },
  232. "circledrawer": {
  233. exports: "CircleDrawer"
  234. }
  235. }
  236. });
  237. require(["domReady", "undomanager", "circledrawer"], function(domReady, UndoManager, CircleDrawer) {
  238. "use strict";
  239. let undoManager,
  240. circleDrawer,
  241. btnUndo,
  242. btnRedo,
  243. btnClear;
  244. undoManager = new UndoManager();
  245. circleDrawer = new CircleDrawer("view", undoManager);
  246. // etcetera
  247. });
  248. ```