├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── bower.json ├── gulpfile.js ├── lib └── simple-undo.js ├── package.json └── tests └── simple-undo.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | /* 2 | * ---------------------------------------------------------------------------- 3 | * "THE BEER-WARE LICENSE" (Revision 42): 4 | * Matthias Jouan wrote this piece of software. 5 | * As long as you retain this notice you can do whatever you want with this 6 | * stuff. 7 | * If we meet some day, and you think this stuff is worth it, you can buy me a 8 | * beer in return. 9 | * ---------------------------------------------------------------------------- 10 | */ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # simple-undo 2 | 3 | [![Build Status](https://travis-ci.org/mattjmattj/simple-undo.svg)](https://travis-ci.org/mattjmattj/simple-undo) 4 | 5 | simple-undo is a very basic javascript undo/redo stack for managing histories of basically anything. 6 | 7 | Initially created to help fixing an issue in [drawingboard.js](https://github.com/Leimi/drawingboard.js/issues/29). 8 | 9 | ## Installation 10 | 11 | ### Bower 12 | 13 | `bower install simple-undo`. 14 | 15 | ### NPM 16 | 17 | `npm install simple-undo` 18 | 19 | ## Usage 20 | 21 | If you are using simple-undo in the browser, a SimpleUndo object is exported in `window` after including simple-undo in your page, so it is very easy to use. 22 | 23 | If you are using simple-undo as a nodejs module, just do `var SimpleUndo = require('simple-undo');` 24 | 25 | ```javascript 26 | 27 | var myObject = {}; 28 | 29 | function myObjectSerializer(done) { 30 | done(JSON.stringify(myObject)); 31 | } 32 | 33 | function myObjectUnserializer(serialized) { 34 | myObject = JSON.parse(serialized); 35 | } 36 | 37 | var history = new SimpleUndo({ 38 | maxLength: 10, 39 | provider: myObjectSerializer 40 | }); 41 | 42 | myObject.foo = 'bar'; 43 | history.save(); 44 | myObject.foo = 'baz'; 45 | history.save(); 46 | 47 | history.undo(myObjectUnserializer); 48 | // myObject.foo == 'bar' 49 | history.redo(myObjectUnserializer); 50 | // myObject.foo == 'baz' 51 | 52 | ``` 53 | 54 | Another example is available on the [GitHub page of the project](http://mattjmattj.github.io/simple-undo/) 55 | 56 | ## Options and API 57 | 58 | Accepted options are 59 | 60 | * `provider` : required. a function to call on `save`, which should provide the current state of the historized object through the given `done` callback 61 | * `maxLength` : the maximum number of items in history 62 | * `onUpdate` : a function to call to notify of changes in history. Will be called on `save`, `undo`, `redo` and `clear` 63 | 64 | SimpleUndo 65 | 66 | * `initialize (initialState)` : registers the initial state of the managed object. If not call the default initial state is NULL 67 | * `save ()` : calls the `provider` and registers whatever it gives 68 | * `undo (callback)` : calls the callback with the previous state of the managed object in history 69 | * `redo (callback)` : calls the callback with the next state of the managed object in history 70 | * `clear ()` : clears the whole history, except the inital state if any 71 | * `count ()` : returns the count of elements in history, apart from the inital state 72 | 73 | ## License 74 | 75 | simple-undo is licensed under the terms of the [Beerware license](LICENSE). 76 | 77 | 2014 - Matthias Jouan 78 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-undo", 3 | "version": "1.0.2", 4 | "authors": [ 5 | "Matthias Jouan " 6 | ], 7 | "description": "a very basic javascript undo/redo stack for managing histories of basically anything", 8 | "main": "./lib/simple-undo.js", 9 | "moduleType": [ 10 | "globals", 11 | "node" 12 | ], 13 | "keywords": [ 14 | "undo", 15 | "redo", 16 | "history" 17 | ], 18 | "license": "THE BEER-WARE LICENSE", 19 | "homepage": "https://github.com/mattjmattj/simple-undo", 20 | "ignore": [ 21 | "**/.*", 22 | "node_modules", 23 | "bower_components", 24 | "tests" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var jshint = require('gulp-jshint'); 2 | var mocha = require('gulp-mocha'); 3 | var gulp = require('gulp'); 4 | 5 | gulp.task('lint', function() { 6 | return gulp.src('./lib/simple-undo.js') 7 | .pipe(jshint()) 8 | .pipe(jshint.reporter('default')); 9 | }); 10 | 11 | gulp.task('test', function() { 12 | return gulp.src('./tests/simple-undo.js', {read: false}) 13 | .pipe(mocha({reporter: 'spec'})); 14 | }); 15 | -------------------------------------------------------------------------------- /lib/simple-undo.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | 'use strict'; 4 | 5 | /** 6 | * SimpleUndo is a very basic javascript undo/redo stack for managing histories of basically anything. 7 | * 8 | * options are: { 9 | * * `provider` : required. a function to call on `save`, which should provide the current state of the historized object through the given "done" callback 10 | * * `maxLength` : the maximum number of items in history 11 | * * `onUpdate` : a function to call to notify of changes in history. Will be called on `save`, `undo`, `redo` and `clear` 12 | * } 13 | * 14 | */ 15 | var SimpleUndo = function(options) { 16 | 17 | var settings = options ? options : {}; 18 | var defaultOptions = { 19 | provider: function() { 20 | throw new Error("No provider!"); 21 | }, 22 | maxLength: 30, 23 | onUpdate: function() {} 24 | }; 25 | 26 | this.provider = (typeof settings.provider != 'undefined') ? settings.provider : defaultOptions.provider; 27 | this.maxLength = (typeof settings.maxLength != 'undefined') ? settings.maxLength : defaultOptions.maxLength; 28 | this.onUpdate = (typeof settings.onUpdate != 'undefined') ? settings.onUpdate : defaultOptions.onUpdate; 29 | 30 | this.initialItem = null; 31 | this.clear(); 32 | }; 33 | 34 | function truncate (stack, limit) { 35 | while (stack.length > limit) { 36 | stack.shift(); 37 | } 38 | } 39 | 40 | SimpleUndo.prototype.initialize = function(initialItem) { 41 | this.stack[0] = initialItem; 42 | this.initialItem = initialItem; 43 | }; 44 | 45 | 46 | SimpleUndo.prototype.clear = function() { 47 | this.stack = [this.initialItem]; 48 | this.position = 0; 49 | this.onUpdate(); 50 | }; 51 | 52 | SimpleUndo.prototype.save = function() { 53 | this.provider(function(current) { 54 | if (this.position >= this.maxLength) truncate(this.stack, this.maxLength); 55 | this.position = Math.min(this.position,this.stack.length - 1); 56 | 57 | this.stack = this.stack.slice(0, this.position + 1); 58 | this.stack.push(current); 59 | this.position++; 60 | this.onUpdate(); 61 | }.bind(this)); 62 | }; 63 | 64 | SimpleUndo.prototype.undo = function(callback) { 65 | if (this.canUndo()) { 66 | var item = this.stack[--this.position]; 67 | this.onUpdate(); 68 | 69 | if (callback) { 70 | callback(item); 71 | } 72 | } 73 | }; 74 | 75 | SimpleUndo.prototype.redo = function(callback) { 76 | if (this.canRedo()) { 77 | var item = this.stack[++this.position]; 78 | this.onUpdate(); 79 | 80 | if (callback) { 81 | callback(item); 82 | } 83 | } 84 | }; 85 | 86 | SimpleUndo.prototype.canUndo = function() { 87 | return this.position > 0; 88 | }; 89 | 90 | SimpleUndo.prototype.canRedo = function() { 91 | return this.position < this.count(); 92 | }; 93 | 94 | SimpleUndo.prototype.count = function() { 95 | return this.stack.length - 1; // -1 because of initial item 96 | }; 97 | 98 | 99 | 100 | 101 | 102 | //exports 103 | // node module 104 | if (typeof module != 'undefined') { 105 | module.exports = SimpleUndo; 106 | } 107 | 108 | // browser global 109 | if (typeof window != 'undefined') { 110 | window.SimpleUndo = SimpleUndo; 111 | } 112 | 113 | })(); 114 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-undo", 3 | "version": "1.0.2", 4 | "description": "a very basic javascript undo/redo stack for managing histories of basically anything", 5 | "main": "./lib/simple-undo.js", 6 | "scripts": { 7 | "test": "node_modules/.bin/gulp test" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/mattjmattj/simple-undo.git" 12 | }, 13 | "keywords": [ 14 | "undo", 15 | "redo", 16 | "history" 17 | ], 18 | "author": "Matthias Jouan", 19 | "license": { 20 | "type": "THE BEER-WARE LICENSE", 21 | "url": "https://fedoraproject.org/wiki/Licensing/Beerware" 22 | }, 23 | "bugs": { 24 | "url": "https://github.com/mattjmattj/simple-undo/issues" 25 | }, 26 | "homepage": "https://github.com/mattjmattj/simple-undo", 27 | "devDependencies": { 28 | "gulp": "^3.8.8", 29 | "gulp-jshint": "^1.8.5", 30 | "gulp-mocha": "^1.1.0", 31 | "jshint": "^2.5.6", 32 | "mocha": "^1.21.4", 33 | "should": "^4.0.4" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/simple-undo.js: -------------------------------------------------------------------------------- 1 | var should = require('should'); 2 | var SimpleUndo = require('../lib/simple-undo'); 3 | 4 | 5 | 6 | describe('SimpleUndo', function() { 7 | 8 | it('should require a provider', function() { 9 | var history = new SimpleUndo(); 10 | history.save.should.throw(); 11 | }); 12 | 13 | it('should contain up to maxLength items', function() { 14 | var count; 15 | 16 | var value = 0; 17 | var provider = function(done) { 18 | done(value++); 19 | } 20 | 21 | var history = new SimpleUndo({ 22 | provider: provider, 23 | maxLength: 3 24 | }); 25 | 26 | history.initialize('initial'); 27 | 28 | history.save(); //0 29 | count = history.count(); 30 | count.should.equal(1); 31 | history.save(); //1 32 | count = history.count(); 33 | count.should.equal(2); 34 | history.save(); //2 35 | count = history.count(); 36 | count.should.equal(3); 37 | 38 | //we reached the limit 39 | history.save(); //3 40 | count = history.count(); 41 | count.should.equal(3); 42 | history.save();//4 43 | count = history.count(); 44 | count.should.equal(3); 45 | 46 | history.undo(); //3 47 | history.undo(); //2 48 | history.undo(function(value){ 49 | value.should.equal(1); 50 | }); 51 | }); 52 | 53 | it('should undo and redo', function() { 54 | 55 | var provided = 0; 56 | var provider = function(done) { 57 | done(provided++); 58 | } 59 | 60 | var history = new SimpleUndo({ 61 | provider: provider, 62 | maxLength: 3 63 | }); 64 | 65 | history.initialize('initial'); 66 | 67 | history.canUndo().should.be.false; 68 | history.canRedo().should.be.false; 69 | 70 | history.save(); //0 71 | history.canUndo().should.be.true; 72 | history.canRedo().should.be.false; 73 | 74 | history.undo(function(value) { 75 | value.should.equal('initial'); 76 | }); 77 | history.canUndo().should.be.false; 78 | history.canRedo().should.be.true; 79 | 80 | history.save(); //1 81 | history.canUndo().should.be.true; 82 | history.canRedo().should.be.false; 83 | 84 | history.save(); //2 85 | history.canUndo().should.be.true; 86 | history.canRedo().should.be.false; 87 | 88 | history.undo(function(value) { 89 | value.should.equal(1); 90 | }); 91 | history.canUndo().should.be.true; 92 | history.canRedo().should.be.true; 93 | 94 | history.redo(function(value) { 95 | value.should.equal(2); 96 | }); 97 | history.canUndo().should.be.true; 98 | history.canRedo().should.be.false; 99 | }); 100 | 101 | it('should call the registered onUpdate callback', function() { 102 | var provider = function(done) { 103 | done(Math.random()); 104 | }; 105 | 106 | var callCount = 0; 107 | var onUpdate = function() { 108 | callCount++; 109 | }; 110 | 111 | var history = new SimpleUndo({ 112 | provider: provider, 113 | onUpdate: onUpdate 114 | }); 115 | 116 | callCount.should.equal(1); 117 | history.save(); 118 | callCount.should.equal(2); 119 | history.undo(); 120 | callCount.should.equal(3); 121 | history.redo(); 122 | callCount.should.equal(4); 123 | history.clear(); 124 | callCount.should.equal(5); 125 | }); 126 | 127 | it('should reset when cleared', function() { 128 | var provider = function(done) { 129 | done(Math.random()); 130 | } 131 | 132 | var history = new SimpleUndo({ 133 | provider: provider, 134 | maxLength: 3 135 | }); 136 | 137 | history.initialize('initial'); 138 | 139 | history.save(); 140 | history.save(); 141 | history.undo(); 142 | history.save(); 143 | history.undo(); 144 | history.redo(); 145 | history.clear(); 146 | 147 | history.count().should.equal(0); 148 | history.save(); 149 | history.undo(function(value){ 150 | value.should.equal('initial'); 151 | }); 152 | 153 | }); 154 | 155 | it('should reserve initial when undo position to be 0 and save', function() { 156 | var count = 0; 157 | var provider = function(done) { 158 | done(count++); 159 | } 160 | 161 | var history = new SimpleUndo({ 162 | provider: provider, 163 | maxLength: 3 164 | }); 165 | 166 | history.initialize('initial'); 167 | history.save(); 168 | history.save(); 169 | history.save(); 170 | history.undo(); 171 | history.undo(); 172 | history.undo(); 173 | 174 | history.canUndo().should.be.false; 175 | history.count().should.equal(3); 176 | history.save(); 177 | history.stack.length.should.equal(2); 178 | history.stack[0].should.equal('initial'); 179 | history.stack[1].should.equal(3); 180 | }); 181 | }) 182 | --------------------------------------------------------------------------------