├── .npmignore ├── .gitattributes ├── bower.json ├── .gitignore ├── package.json ├── LICENSE.md ├── CITATION.cff ├── gulpfile.js ├── demo.html ├── README.md └── cytoscape-undo-redo.js /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cytoscape.js-undo-redo", 3 | "description": "Manages undo/redo actions", 4 | "main": "cytoscape.js-undo-redo.js", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/iVis-at-Bilkent/cytoscape.js-undo-redo.git" 8 | }, 9 | "ignore": [ 10 | "**/.*", 11 | "node_modules", 12 | "bower_components", 13 | "test", 14 | "tests" 15 | ], 16 | "keywords": [ 17 | "cytoscape", 18 | "cyext" 19 | ], 20 | "license": "MIT" 21 | } 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Windows image file caches 2 | Thumbs.db 3 | ehthumbs.db 4 | 5 | # Folder config file 6 | Desktop.ini 7 | 8 | # Recycle Bin used on file shares 9 | $RECYCLE.BIN/ 10 | 11 | # Windows Installer files 12 | *.cab 13 | *.msi 14 | *.msm 15 | *.msp 16 | 17 | # Windows shortcuts 18 | *.lnk 19 | 20 | # ========================= 21 | # Operating System Files 22 | # ========================= 23 | 24 | # OSX 25 | # ========================= 26 | 27 | .DS_Store 28 | .AppleDouble 29 | .LSOverride 30 | 31 | # Thumbnails 32 | ._* 33 | npm-debug.log 34 | # Files that might appear in the root of a volume 35 | .DocumentRevisions-V100 36 | .fseventsd 37 | .Spotlight-V100 38 | .TemporaryItems 39 | .Trashes 40 | .VolumeIcon.icns 41 | 42 | # Directories potentially created on remote AFP share 43 | .AppleDB 44 | .AppleDesktop 45 | Network Trash Folder 46 | Temporary Items 47 | .apdisk 48 | node_modules 49 | .idea/ 50 | /nbproject/private/ 51 | /nbproject/* -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cytoscape-undo-redo", 3 | "version": "1.3.3", 4 | "description": "Manages undo-redo actions", 5 | "main": "cytoscape-undo-redo.js", 6 | "spm": { 7 | "main": "cytoscape-undo-redo.js" 8 | }, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/iVis-at-Bilkent/cytoscape.js-undo-redo.git" 15 | }, 16 | "keywords": [ 17 | "cytoscape", 18 | "cyext" 19 | ], 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/iVis-at-Bilkent/cytoscape.js-undo-redo/issues" 23 | }, 24 | "homepage": "https://github.com/iVis-at-Bilkent/cytoscape.js-undo-redo", 25 | "devDependencies": { 26 | "gulp": "^3.8.8", 27 | "gulp-jshint": "^1.8.5", 28 | "gulp-prompt": "^0.1.1", 29 | "gulp-replace": "^0.4.0", 30 | "gulp-shell": "^0.2.9", 31 | "jshint-stylish": "^1.0.0", 32 | "run-sequence": "^1.2.1" 33 | }, 34 | "peerDependencies": { 35 | "cytoscape": "^3.3.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 iVis-at-Bilkent 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | cff-version: 1.2.0 2 | message: "If you use this software, please cite it as below." 3 | authors: 4 | - family-names: "Dogrusoz" 5 | given-names: "Ugur" 6 | orcid: "https://orcid.org/0000-0002-7153-0784" 7 | - family-names: "Karacelik" 8 | given-names: "Alper" 9 | orcid: "https://orcid.org/0000-0000-0000-0000" 10 | - family-names: "Safarli" 11 | given-names: "Ilkin" 12 | - family-names: "Balci" 13 | given-names: "Hasan" 14 | orcid: "https://orcid.org/0000-0001-8319-7758" 15 | - family-names: "Dervishi" 16 | given-names: "Leonard" 17 | - family-names: "Siper" 18 | given-names: "Metin Can" 19 | title: "cytoscape-undo-redo" 20 | version: 1.3.3 21 | date-released: 2020-03-29 22 | url: "https://github.com/iVis-at-Bilkent/cytoscape.js-undo-redo" 23 | preferred-citation: 24 | type: article 25 | authors: 26 | - family-names: "Dogrusoz" 27 | given-names: "Ugur" 28 | orcid: "https://orcid.org/0000-0002-7153-0784" 29 | - family-names: "Karacelik" 30 | given-names: "Alper" 31 | orcid: "https://orcid.org/0000-0000-0000-0000" 32 | - family-names: "Safarli" 33 | given-names: "Ilkin" 34 | - family-names: "Balci" 35 | given-names: "Hasan" 36 | orcid: "https://orcid.org/0000-0001-8319-7758" 37 | - family-names: "Dervishi" 38 | given-names: "Leonard" 39 | - family-names: "Siper" 40 | given-names: "Metin Can" 41 | doi: "10.1371/journal.pone.0197238" 42 | journal: "PLOS ONE" 43 | month: 5 44 | start: 1 # First page number 45 | end: 18 # Last page number 46 | title: "Efficient methods and readily customizable libraries for managing complexity of large networks" 47 | issue: 5 48 | volume: 13 49 | year: 2018 50 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var path = require('path'); 3 | var replace = require('gulp-replace'); 4 | var child_process = require('child_process'); 5 | var fs = require('fs'); 6 | var shell = require('gulp-shell'); 7 | var jshint = require('gulp-jshint'); 8 | var jshStylish = require('jshint-stylish'); 9 | var exec = require('child_process').exec; 10 | var runSequence = require('run-sequence'); 11 | var prompt = require('gulp-prompt'); 12 | var version; 13 | 14 | gulp.task('default', [], function( next ){ 15 | console.log('You must explicitly call `gulp publish` to publish the extension'); 16 | next(); 17 | }); 18 | 19 | gulp.task('publish', [], function( next ){ 20 | runSequence('confver', /*'lint',*/ 'pkgver', 'push', 'tag', 'npm', next); 21 | }); 22 | 23 | gulp.task('confver', ['version'], function(){ 24 | return gulp.src('.') 25 | .pipe( prompt.confirm({ message: 'Are you sure version `' + version + '` is OK to publish?' }) ) 26 | ; 27 | }); 28 | 29 | gulp.task('version', function( next ){ 30 | var now = new Date(); 31 | version = process.env['VERSION']; 32 | 33 | if( version ){ 34 | done(); 35 | } else { 36 | exec('git rev-parse HEAD', function( error, stdout, stderr ){ 37 | var sha = stdout.substring(0, 10); // shorten so not huge filename 38 | 39 | version = [ 'snapshot', sha, +now ].join('-'); 40 | done(); 41 | }); 42 | } 43 | 44 | function done(){ 45 | console.log('Using version number `%s` for building', version); 46 | next(); 47 | } 48 | 49 | }); 50 | 51 | gulp.task('pkgver', ['version'], function(){ 52 | return gulp.src([ 53 | 'package.json', 54 | 'bower.json' 55 | ]) 56 | .pipe( replace(/\"version\"\:\s*\".*?\"/, '"version": "' + version + '"') ) 57 | 58 | .pipe( gulp.dest('./') ) 59 | ; 60 | }); 61 | 62 | gulp.task('push', shell.task([ 63 | 'git add -A', 64 | 'git commit -m "pushing changes for v$VERSION release" || echo Nothing to commit', 65 | 'git push || echo Nothing to push' 66 | ])); 67 | 68 | gulp.task('tag', shell.task([ 69 | 'git tag -a $VERSION -m "tagging v$VERSION"', 70 | 'git push origin $VERSION' 71 | ])); 72 | 73 | gulp.task('npm', shell.task([ 74 | 'npm publish .' 75 | ])); 76 | 77 | // http://www.jshint.com/docs/options/ 78 | gulp.task('lint', function(){ 79 | return gulp.src( 'cytoscape-*.js' ) 80 | .pipe( jshint({ 81 | funcscope: true, 82 | laxbreak: true, 83 | loopfunc: true, 84 | strict: true, 85 | unused: 'vars', 86 | eqnull: true, 87 | sub: true, 88 | shadow: true, 89 | laxcomma: true 90 | }) ) 91 | 92 | .pipe( jshint.reporter(jshStylish) ) 93 | 94 | .pipe( jshint.reporter('fail') ) 95 | ; 96 | }); 97 | -------------------------------------------------------------------------------- /demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | cytoscape-undo-redo.js demo 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 36 | 37 | 153 | 154 | 155 | 156 |

cytoscape.js-undo-redo demo

157 | 158 |

159 | DEL to delete selected, CTRL+Z to undo, CTRL+Y to redo
160 | Test batch of actions: 161 |

162 | 163 |
164 |
165 | Log 166 |
167 | 168 |
169 |
170 | 171 | 172 | 173 | 174 | 175 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | cytoscape-undo-redo 2 | ================================================================================ 3 | 4 | ## Description 5 | A Cytsocape.js extension to control actions on Cytoscape.js graph, also providing built-in functionalities for common Cytoscape.js operations like dragging nodes, adding/removing nodes, etc. distributed under [The MIT License](https://opensource.org/licenses/MIT). 6 | 7 | Please cite the following paper when using this extension: 8 | 9 | U. Dogrusoz , A. Karacelik, I. Safarli, H. Balci, L. Dervishi, and M.C. Siper, "[Efficient methods and readily customizable libraries for managing complexity of large networks](https://doi.org/10.1371/journal.pone.0197238)", PLoS ONE, 13(5): e0197238, 2018. 10 | 11 | ## Demo 12 | 13 | Click [here](http://ivis-at-bilkent.github.io/cytoscape.js-undo-redo/demo.html) for demo 14 | 15 | ## API 16 | 17 | ```javascript 18 | var cy = cytoscape({...}); 19 | 20 | var ur = cy.undoRedo(options); 21 | 22 | ``` 23 | 24 | 25 | `cy.undoRedo(options, dontInit)` 26 | Sets options. Also, dontInit can be left blank and is to be used in extensions to set default actions of an extension. 27 | 28 | `ur.action( actionName, actionFunction, undoFunction)` 29 | Register action with its undo function & action name. actionFunction's return value will be used to call undoFunction by argument and vice versa. This function is chainable: `ur.action(...).action(...)` 30 | 31 | 32 | `ur.do(actionName, args)` 33 | Calls registered function with action name actionName via actionFunction(args) 34 | * `args.firstTime` is reserved. The reason behind is on first call of actionFunction 35 | takes a parameter with property `args.firstTime = true` (if args is object or array). After first call, it's set to false. 36 | 37 | `ur.undo()` 38 | Undo last action. Returns arguments that are passed to redo. 39 | 40 | `ur.redo()` 41 | Redo last action. Returns arguments that are passed to undo. 42 | 43 | `ur.undoAll()` 44 | Undo all actions in undo stack. 45 | 46 | `ur.redoAll()` 47 | Redo all actions in redo stack. 48 | 49 | `cy.on("undo", function(actionName, args){} )` 50 | Calls registered function with action name actionName via actionFunction(args) 51 | 52 | `cy.on("redo", function(actionName, args){} )` 53 | Calls registered function with action name actionName via actionFunction(args) 54 | *Note that args are returned from opposite action like (undo => redo || redo => undo) 55 | 56 | `ur.isUndoStackEmpty()` 57 | Get whether undo stack is empty (namely is undoable) 58 | 59 | `ur.isRedoStackEmpty()` 60 | Get whether undo stack is empty (namely is redoable) 61 | 62 | `ur.getUndoStack()` 63 | Gets actions (with their args) in undo stack 64 | 65 | `ur.getRedoStack()` 66 | Gets actions (with their args) in redo stack 67 | 68 | `ur.reset(undos, redos)` 69 | If arguments are provided, overrides undo and redo stacks. Otherwise, undo and redo stacks are cleared. 70 | 71 | 72 | ## Default Options 73 | ```javascript 74 | var options = { 75 | isDebug: false, // Debug mode for console messages 76 | actions: {},// actions to be added 77 | undoableDrag: true, // Whether dragging nodes are undoable can be a function as well 78 | stackSizeLimit: undefined, // Size limit of undo stack, note that the size of redo stack cannot exceed size of undo stack 79 | ready: function () { // callback when undo-redo is ready 80 | 81 | } 82 | } 83 | 84 | var ur = cy.undoRedo(options); // Can also be set whenever wanted. 85 | ``` 86 | 87 | 88 | ## Events 89 | 90 | Parameters:
91 | actionName: Name of the action.
92 | args: Arguments passed to the action.
93 | res: The value returned when the function is executed. This value is to be passed to redo function in afterUndo case and it will be passed to undo function in afterDo/afterRedo cases.
94 | 95 | `.on("beforeUndo", function(event, actionName, args){ })` 96 | 97 | `.on("afterUndo", function(event, actionName, args, res){ })` 98 | 99 | `.on("beforeRedo", function(event, actionName, args){ })` 100 | 101 | `.on("afterRedo", function(event, actionName, args, res){ })` 102 | 103 | `.on("beforeDo", function(event, actionName, args){ })` 104 | 105 | `.on("afterDo", function(event, actionName, args, res){ })` 106 | 107 | 108 | 109 | ## Default Actions (Undoable/Redoable) 110 | * Default actions can be run by the same way like `ur.do("remove", "#spec")` 111 | * Undoable dragging can be disabled through options `undoableDrag: false` 112 | 113 | `.do("add", eleObj)` http://js.cytoscape.org/#cy.add 114 | 115 | `.do("remove", eles/selector)` http://js.cytoscape.org/#cy.remove 116 | 117 | `.do("layout", args)` http://js.cytoscape.org/#core/layout 118 | 119 | ```javascript 120 | var args = { 121 | options: {}, // layout options 122 | eles: null // if not null eles.layout will be called. 123 | } 124 | ``` 125 | 126 | `.do("changeParent", args)` http://js.cytoscape.org/#eles.move (Just for the nodes and regards the new positions of the nodes as well) 127 | 128 | ```javascript 129 | var args = { 130 | parentData: parentData, // It keeps the newParentId (Just an id for each nodes for the first time) 131 | nodes: nodes, // Nodes to move the new parent 132 | posDiffX: diffX, // How the positions of the nodes will change in 'X' axis after they are moved the new parent 133 | posDiffY: diffY, // How the positions of the nodes will change in 'Y' axis after they are moved the new parent 134 | callback: function(eles) {} // optional - a function to be called after the change has occured, on the newly created elements 135 | } 136 | ``` 137 | 138 | * Following actions take argument(s) instead of extending 139 | 140 | `.do("restore", eles/selector)` http://js.cytoscape.org/#eles.restore 141 | 142 | `.do("clone", eles/selector)` http://js.cytoscape.org/#eles.restore 143 | 144 | `.do("select", eles/selector)` http://js.cytoscape.org/#eles.select 145 | 146 | `.do("unselect", eles/selector)` http://js.cytoscape.org/#eles.unselect 147 | 148 | `.do("move", args)` http://js.cytoscape.org/#eles.move 149 | 150 | ```javascript 151 | var args = { 152 | eles: ..., // eles/selector 153 | location: ... // as is in docs 154 | } 155 | ``` 156 | 157 | * The `batch` action can execute several actions at the same time. Those actions can then be undone as a whole. 158 | 159 | `.do("batch", actionList)` 160 | 161 | ```javascript 162 | var actionList = [{ 163 | name: ..., // name of the action 164 | param: ... // object containing the parameters as you would pass them to said action 165 | }, 166 | {...}, // a second action to be executed 167 | ... 168 | ] 169 | ``` 170 | 171 | ## Example 172 | ```javascript 173 | function deleteEles(eles){ 174 | return eles.remove(); 175 | } 176 | function restoreEles(eles){ 177 | return eles.restore(); 178 | } 179 | ur.action("deleteEles", deleteEles, restoreEles); // register 180 | 181 | var selecteds = cy.$(":selected"); 182 | ur.do("deleteEles", selecteds); // 183 | 184 | ur.undo(); 185 | ur.redo(); 186 | ``` 187 | * Note that default `remove` default action above has the same functionality and also supports string selectors like `#spec`. 188 | 189 | 190 | ## Dependencies 191 | 192 | * Cytoscape.js ^3.3.0 193 | 194 | 195 | 196 | ## Usage instructions 197 | 198 | Download the library: 199 | * via npm: `npm install cytoscape-undo-redo`, 200 | * via bower: `bower install cytoscape-undo-redo`, or 201 | * via direct download in the repository (probably from a tag). 202 | 203 | `require()` the library as appropriate for your project: 204 | 205 | CommonJS: 206 | ```js 207 | var cytoscape = require('cytoscape'); 208 | var undoRedo = require('cytoscape-undo-redo'); 209 | 210 | undoRedo( cytoscape ); // register extension 211 | ``` 212 | 213 | AMD: 214 | ```js 215 | require(['cytoscape', 'cytoscape-undo-redo'], function( cytoscape, undoRedo ){ 216 | undoRedo( cytoscape ); // register extension 217 | }); 218 | ``` 219 | 220 | Plain HTML/JS has the extension registered for you automatically, because no `require()` is needed. 221 | 222 | 223 | 224 | ## Publishing instructions 225 | 226 | This project is set up to automatically be published to npm and bower. To publish: 227 | 228 | 1. Set the version number environment variable: `export VERSION=1.2.3` 229 | 1. Publish: `gulp publish` 230 | 1. If publishing to bower for the first time, you'll need to run `bower register cytoscape-undo-redo https://github.com/iVis-at-Bilkent/cytoscape.js-undo-redo.git` 231 | 232 | ## Team 233 | 234 | * [Selim Firat Yilmaz](https://github.com/mrsfy), [Metin Can Siper](https://github.com/metincansiper), [Ugur Dogrusoz](https://github.com/ugurdogrusoz) of [i-Vis at Bilkent University](http://www.cs.bilkent.edu.tr/~ivis) 235 | -------------------------------------------------------------------------------- /cytoscape-undo-redo.js: -------------------------------------------------------------------------------- 1 | ;(function () { 2 | 'use strict'; 3 | 4 | // registers the extension on a cytoscape lib ref 5 | var register = function (cytoscape) { 6 | 7 | if (!cytoscape) { 8 | return; 9 | } // can't register if cytoscape unspecified 10 | 11 | // Get scratch pad reserved for this extension on the given element or the core if 'name' parameter is not set, 12 | // if the 'name' parameter is set then return the related property in the scratch instead of the whole scratchpad 13 | function getScratch (eleOrCy, name) { 14 | 15 | if (eleOrCy.scratch("_undoRedo") === undefined) { 16 | eleOrCy.scratch("_undoRedo", {}); 17 | } 18 | 19 | var scratchPad = eleOrCy.scratch("_undoRedo"); 20 | 21 | return ( name === undefined ) ? scratchPad : scratchPad[name]; 22 | } 23 | 24 | // Set the a field (described by 'name' parameter) of scratchPad (that is reserved for this extension 25 | // on an element or the core) to the given value (by 'val' parameter) 26 | function setScratch (eleOrCy, name, val) { 27 | 28 | var scratchPad = getScratch(eleOrCy); 29 | scratchPad[name] = val; 30 | eleOrCy.scratch("_undoRedo", scratchPad); 31 | } 32 | 33 | // Generate an instance of the extension for the given cy instance 34 | function generateInstance (cy) { 35 | var instance = {}; 36 | 37 | instance.options = { 38 | isDebug: false, // Debug mode for console messages 39 | actions: {},// actions to be added 40 | undoableDrag: true, // Whether dragging nodes are undoable can be a function as well 41 | stackSizeLimit: undefined, // Size limit of undo stack, note that the size of redo stack cannot exceed size of undo stack 42 | beforeUndo: function () { // callback before undo is triggered. 43 | 44 | }, 45 | afterUndo: function () { // callback after undo is triggered. 46 | 47 | }, 48 | beforeRedo: function () { // callback before redo is triggered. 49 | 50 | }, 51 | afterRedo: function () { // callback after redo is triggered. 52 | 53 | }, 54 | ready: function () { 55 | 56 | } 57 | }; 58 | 59 | instance.actions = {}; 60 | 61 | instance.undoStack = []; 62 | 63 | instance.redoStack = []; 64 | 65 | //resets undo and redo stacks 66 | instance.reset = function(undos, redos) 67 | { 68 | this.undoStack = undos || []; 69 | this.redoStack = redos || []; 70 | } 71 | 72 | // Undo last action 73 | instance.undo = function () { 74 | if (!this.isUndoStackEmpty()) { 75 | 76 | var action = this.undoStack.pop(); 77 | cy.trigger("beforeUndo", [action.name, action.args]); 78 | 79 | var res = this.actions[action.name]._undo(action.args); 80 | 81 | this.redoStack.push({ 82 | name: action.name, 83 | args: res 84 | }); 85 | 86 | cy.trigger("afterUndo", [action.name, action.args, res]); 87 | return res; 88 | } else if (this.options.isDebug) { 89 | console.log("Undoing cannot be done because undo stack is empty!"); 90 | } 91 | }; 92 | 93 | // Redo last action 94 | instance.redo = function () { 95 | 96 | if (!this.isRedoStackEmpty()) { 97 | var action = this.redoStack.pop(); 98 | 99 | cy.trigger(action.firstTime ? "beforeDo" : "beforeRedo", [action.name, action.args]); 100 | 101 | if (!action.args) 102 | action.args = {}; 103 | action.args.firstTime = action.firstTime ? true : false; 104 | 105 | var res = this.actions[action.name]._do(action.args); 106 | 107 | this.undoStack.push({ 108 | name: action.name, 109 | args: res 110 | }); 111 | 112 | if (this.options.stackSizeLimit != undefined && this.undoStack.length > this.options.stackSizeLimit ) { 113 | this.undoStack.shift(); 114 | } 115 | 116 | cy.trigger(action.firstTime ? "afterDo" : "afterRedo", [action.name, action.args, res]); 117 | return res; 118 | } else if (this.options.isDebug) { 119 | console.log("Redoing cannot be done because redo stack is empty!"); 120 | } 121 | 122 | }; 123 | 124 | // Calls registered function with action name actionName via actionFunction(args) 125 | instance.do = function (actionName, args) { 126 | 127 | this.redoStack.length = 0; 128 | this.redoStack.push({ 129 | name: actionName, 130 | args: args, 131 | firstTime: true 132 | }); 133 | 134 | return this.redo(); 135 | }; 136 | 137 | // Undo all actions in undo stack 138 | instance.undoAll = function() { 139 | 140 | while( !this.isUndoStackEmpty() ) { 141 | this.undo(); 142 | } 143 | }; 144 | 145 | // Redo all actions in redo stack 146 | instance.redoAll = function() { 147 | 148 | while( !this.isRedoStackEmpty() ) { 149 | this.redo(); 150 | } 151 | }; 152 | 153 | // Register action with its undo function & action name. 154 | instance.action = function (actionName, _do, _undo) { 155 | 156 | this.actions[actionName] = { 157 | _do: _do, 158 | _undo: _undo 159 | }; 160 | 161 | 162 | return this; 163 | }; 164 | 165 | // Removes action stated with actionName param 166 | instance.removeAction = function (actionName) { 167 | delete this.actions[actionName]; 168 | }; 169 | 170 | // Gets whether undo stack is empty 171 | instance.isUndoStackEmpty = function () { 172 | return (this.undoStack.length === 0); 173 | }; 174 | 175 | // Gets whether redo stack is empty 176 | instance.isRedoStackEmpty = function () { 177 | return (this.redoStack.length === 0); 178 | }; 179 | 180 | // Gets actions (with their args) in undo stack 181 | instance.getUndoStack = function () { 182 | return this.undoStack; 183 | }; 184 | 185 | // Gets actions (with their args) in redo stack 186 | instance.getRedoStack = function () { 187 | return this.redoStack; 188 | }; 189 | 190 | return instance; 191 | } 192 | 193 | // design implementation 194 | cytoscape("core", "undoRedo", function (options, dontInit) { 195 | var cy = this; 196 | var instance = getScratch(cy, 'instance') || generateInstance(cy); 197 | setScratch(cy, 'instance', instance); 198 | 199 | if (options) { 200 | for (var key in options) 201 | if (instance.options.hasOwnProperty(key)) 202 | instance.options[key] = options[key]; 203 | 204 | if (options.actions) 205 | for (var key in options.actions) 206 | instance.actions[key] = options.actions[key]; 207 | 208 | } 209 | 210 | if (!getScratch(cy, 'isInitialized') && !dontInit) { 211 | 212 | var defActions = defaultActions(cy); 213 | for (var key in defActions) 214 | instance.actions[key] = defActions[key]; 215 | 216 | 217 | setDragUndo(cy, instance.options.undoableDrag); 218 | setScratch(cy, 'isInitialized', true); 219 | } 220 | 221 | instance.options.ready(); 222 | 223 | return instance; 224 | 225 | }); 226 | 227 | function setDragUndo(cy, undoable) { 228 | var lastMouseDownNodeInfo = null; 229 | 230 | cy.on("grab", "node", function () { 231 | if (typeof undoable === 'function' ? undoable.call(this) : undoable) { 232 | lastMouseDownNodeInfo = {}; 233 | lastMouseDownNodeInfo.lastMouseDownPosition = { 234 | x: this.position("x"), 235 | y: this.position("y") 236 | }; 237 | lastMouseDownNodeInfo.node = this; 238 | } 239 | }); 240 | cy.on("free", "node", function () { 241 | 242 | var instance = getScratch(cy, 'instance'); 243 | 244 | if (typeof undoable === 'function' ? undoable.call(this) : undoable) { 245 | if (lastMouseDownNodeInfo == null) { 246 | return; 247 | } 248 | var node = lastMouseDownNodeInfo.node; 249 | var lastMouseDownPosition = lastMouseDownNodeInfo.lastMouseDownPosition; 250 | var mouseUpPosition = { 251 | x: node.position("x"), 252 | y: node.position("y") 253 | }; 254 | if (mouseUpPosition.x != lastMouseDownPosition.x || 255 | mouseUpPosition.y != lastMouseDownPosition.y) { 256 | var positionDiff = { 257 | x: mouseUpPosition.x - lastMouseDownPosition.x, 258 | y: mouseUpPosition.y - lastMouseDownPosition.y 259 | }; 260 | 261 | var nodes; 262 | if (node.selected()) { 263 | nodes = cy.nodes(":visible").filter(":selected"); 264 | } 265 | else { 266 | nodes = cy.collection([node]); 267 | } 268 | 269 | var param = { 270 | positionDiff: positionDiff, 271 | nodes: nodes, move: false 272 | }; 273 | 274 | instance.do("drag", param); 275 | 276 | lastMouseDownNodeInfo = null; 277 | } 278 | } 279 | }); 280 | } 281 | 282 | // Default actions 283 | function defaultActions(cy) { 284 | 285 | function getTopMostNodes(nodes) { 286 | var nodesMap = {}; 287 | for (var i = 0; i < nodes.length; i++) { 288 | nodesMap[nodes[i].id()] = true; 289 | } 290 | var roots = nodes.filter(function (ele, i) { 291 | if(typeof ele === "number") { 292 | ele = i; 293 | } 294 | var parent = ele.parent()[0]; 295 | while(parent != null){ 296 | if(nodesMap[parent.id()]){ 297 | return false; 298 | } 299 | parent = parent.parent()[0]; 300 | } 301 | return true; 302 | }); 303 | 304 | return roots; 305 | } 306 | 307 | function moveNodes(positionDiff, nodes, notCalcTopMostNodes) { 308 | var topMostNodes = notCalcTopMostNodes?nodes:getTopMostNodes(nodes); 309 | for (var i = 0; i < topMostNodes.length; i++) { 310 | var node = topMostNodes[i]; 311 | var oldX = node.position("x"); 312 | var oldY = node.position("y"); 313 | //Only simple nodes are moved since the movement of compounds caused the position to be moved twice 314 | if (!node.isParent()) 315 | { 316 | node.position({ 317 | x: oldX + positionDiff.x, 318 | y: oldY + positionDiff.y 319 | }); 320 | } 321 | var children = node.children(); 322 | moveNodes(positionDiff, children, true); 323 | } 324 | } 325 | 326 | function getEles(_eles) { 327 | return (typeof _eles === "string") ? cy.$(_eles) : _eles; 328 | } 329 | 330 | function restoreEles(_eles) { 331 | return getEles(_eles).restore(); 332 | } 333 | 334 | 335 | function returnToPositions(positions) { 336 | var currentPositions = {}; 337 | cy.nodes().not(":parent").positions(function (ele, i) { 338 | if(typeof ele === "number") { 339 | ele = i; 340 | } 341 | 342 | currentPositions[ele.id()] = { 343 | x: ele.position("x"), 344 | y: ele.position("y") 345 | }; 346 | var pos = positions[ele.id()]; 347 | return { 348 | x: pos.x, 349 | y: pos.y 350 | }; 351 | }); 352 | 353 | return currentPositions; 354 | } 355 | 356 | function getNodePositions() { 357 | var positions = {}; 358 | var nodes = cy.nodes(); 359 | for (var i = 0; i < nodes.length; i++) { 360 | var node = nodes[i]; 361 | positions[node.id()] = { 362 | x: node.position("x"), 363 | y: node.position("y") 364 | }; 365 | } 366 | return positions; 367 | } 368 | 369 | function changeParentOld(param) { 370 | var result = { 371 | }; 372 | // If this is first time we should move the node to its new parent and relocate it by given posDiff params 373 | // else we should remove the moved eles and restore the eles to restore 374 | if (param.firstTime) { 375 | var newParentId = param.parentData == undefined ? null : param.parentData; 376 | // These eles includes the nodes and their connected edges and will be removed in nodes.move(). 377 | // They should be restored in undo 378 | var withDescendant = param.nodes.union(param.nodes.descendants()); 379 | result.elesToRestore = withDescendant.union(withDescendant.connectedEdges()); 380 | // These are the eles created by nodes.move(), they should be removed in undo. 381 | result.movedEles = param.nodes.move({"parent": newParentId}); 382 | 383 | var posDiff = { 384 | x: param.posDiffX, 385 | y: param.posDiffY 386 | }; 387 | 388 | moveNodes(posDiff, result.movedEles); 389 | } 390 | else { 391 | result.elesToRestore = param.movedEles.remove(); 392 | result.movedEles = param.elesToRestore.restore(); 393 | } 394 | 395 | if (param.callback) { 396 | result.callback = param.callback; // keep the provided callback so it can be reused after undo/redo 397 | param.callback(result.movedEles); // apply the callback on newly created elements 398 | } 399 | 400 | return result; 401 | } 402 | 403 | function changeParentNew(param) { 404 | var result = { 405 | }; 406 | // If this is first time we should move the node to its new parent and relocate it by given posDiff params 407 | // else we should remove the moved eles and restore the eles to restore 408 | if (param.firstTime) { 409 | var newParentId = param.parentData == undefined ? null : param.parentData; 410 | // These eles includes the nodes and their connected edges and will be removed in nodes.move(). 411 | // They should be restored in undo 412 | var withDescendant = param.nodes.union(param.nodes.descendants()); 413 | var parentData = {}; 414 | withDescendant.forEach(function(ele){ 415 | if(ele.parent().id()) 416 | parentData[ele.id()] = ele.parent(); 417 | else 418 | parentData[ele.id()] = null; 419 | }); 420 | var positionData = {}; 421 | withDescendant.forEach(function(ele){ 422 | positionData[ele.id()] = {}; 423 | positionData[ele.id()].x = ele.position('x'); 424 | positionData[ele.id()].y = ele.position('y'); 425 | }); 426 | result.oldParent = parentData; 427 | result.oldPosition = positionData; 428 | result.newParent = newParentId; 429 | result.movedEles = withDescendant; 430 | param.nodes.move({"parent": newParentId}).nodes(); 431 | var posDiff = { 432 | x: param.posDiffX, 433 | y: param.posDiffY 434 | }; 435 | 436 | moveNodes(posDiff, result.movedEles); 437 | } 438 | else { 439 | result.oldParent = {}; 440 | param.movedEles.forEach(function(ele){ 441 | if(ele.parent().id()) 442 | result.oldParent[ele.id()] = ele.parent(); 443 | else 444 | result.oldParent[ele.id()] = null; 445 | }); 446 | result.oldPosition = {}; 447 | param.movedEles.forEach(function(ele){ 448 | result.oldPosition[ele.id()] = {}; 449 | result.oldPosition[ele.id()].x = ele.position("x"); 450 | result.oldPosition[ele.id()].y = ele.position("y"); 451 | }); 452 | result.newParent = param.oldParent; 453 | result.movedEles = param.movedEles; 454 | result.movedEles.forEach(function(ele){ 455 | if(typeof result.newParent !== 'object') 456 | ele.move({'parent': result.newParent}); 457 | else if(result.newParent[ele.id()] == null) 458 | ele.move({'parent': null}); 459 | else 460 | ele.move({'parent': result.newParent[ele.id()].id()}); 461 | 462 | ele.position(param.oldPosition[ele.id()]); 463 | }); 464 | } 465 | 466 | if (param.callback) { 467 | result.callback = param.callback; // keep the provided callback so it can be reused after undo/redo 468 | param.callback(result.movedEles); // apply the callback on newly created elements 469 | } 470 | 471 | return result; 472 | } 473 | 474 | // function registered in the defaultActions below 475 | // to be used like .do('batch', actionList) 476 | // allows to apply any quantity of registered action in one go 477 | // the whole batch can be undone/redone with one key press 478 | function batch (actionList, doOrUndo) { 479 | var tempStack = []; // corresponds to the results of every action queued in actionList 480 | var instance = getScratch(cy, 'instance'); // get extension instance through cy 481 | var actions = instance.actions; 482 | 483 | // here we need to check in advance if all the actions provided really correspond to available functions 484 | // if one of the action cannot be executed, the whole batch is corrupted because we can't go back after 485 | for (var i = 0; i < actionList.length; i++) { 486 | var action = actionList[i]; 487 | if (!actions.hasOwnProperty(action.name)) { 488 | throw "Action " + action.name + " does not exist as an undoable function"; 489 | } 490 | } 491 | 492 | for (var i = 0; i < actionList.length; i++) { 493 | var action = actionList[i]; 494 | // firstTime property is automatically injected into actionList by the do() function 495 | // we use that to pass it down to the actions in the batch 496 | action.param.firstTime = actionList.firstTime; 497 | var actionResult; 498 | if (doOrUndo == "undo") { 499 | actionResult = actions[action.name]._undo(action.param); 500 | } 501 | else { 502 | actionResult = actions[action.name]._do(action.param); 503 | } 504 | 505 | tempStack.unshift({ 506 | name: action.name, 507 | param: actionResult 508 | }); 509 | } 510 | 511 | return tempStack; 512 | }; 513 | 514 | return { 515 | "add": { 516 | _do: function (eles) { 517 | return eles.firstTime ? cy.add(eles) : restoreEles(eles); 518 | }, 519 | _undo: cy.remove 520 | }, 521 | "remove": { 522 | _do: cy.remove, 523 | _undo: restoreEles 524 | }, 525 | "restore": { 526 | _do: restoreEles, 527 | _undo: cy.remove 528 | }, 529 | "select": { 530 | _do: function (_eles) { 531 | return getEles(_eles).select(); 532 | }, 533 | _undo: function (_eles) { 534 | return getEles(_eles).unselect(); 535 | } 536 | }, 537 | "unselect": { 538 | _do: function (_eles) { 539 | return getEles(_eles).unselect(); 540 | }, 541 | _undo: function (_eles) { 542 | return getEles(_eles).select(); 543 | } 544 | }, 545 | "move": { 546 | _do: function (args) { 547 | var eles = getEles(args.eles); 548 | var nodes = eles.nodes(); 549 | var edges = eles.edges(); 550 | 551 | var oldNodesParents = []; 552 | var oldEdgesSources = []; 553 | var oldEdgesTargets = []; 554 | 555 | nodes.forEach(function(node){ 556 | oldNodesParents.push(node.parent().length > 1 ? node.parent().id() : null); 557 | }); 558 | edges.forEach(function(edge){ 559 | oldEdgesSources.push(edge.source().id()); 560 | oldEdgesTargets.push(edge.target().id()); 561 | }) 562 | 563 | return { 564 | oldNodesParents: oldNodesParents, 565 | newNodes: nodes.move(args.location), 566 | oldEdgesSources: oldEdgesSources, 567 | oldEdgesTargets: oldEdgesTargets, 568 | newEdges: edges.move(args.location) 569 | }; 570 | }, 571 | _undo: function (eles) { 572 | var newEles = cy.collection(); 573 | var location = {}; 574 | if (eles.newNodes.length > 0) { 575 | location.parent = eles.newNodes[0].parent().id(); 576 | 577 | for (var i = 0; i < eles.newNodes.length; i++) { 578 | var newNode = eles.newNodes[i].move({ 579 | parent: eles.oldNodesParents[i] 580 | }); 581 | newEles = newEles.union(newNode); 582 | } 583 | } else { 584 | location.source = eles.newEdges[0].source().id(); 585 | location.target = eles.newEdges[0].target().id(); 586 | 587 | for (var i = 0; i < eles.newEdges.length; i++) { 588 | var newEdge = eles.newEdges[i].move({ 589 | source: eles.oldEdgesSources[i], 590 | target: eles.oldEdgesTargets[i] 591 | }); 592 | newEles = newEles.union(newEdge); 593 | } 594 | } 595 | return { 596 | eles: newEles, 597 | location: location 598 | }; 599 | } 600 | }, 601 | "drag": { 602 | _do: function (args) { 603 | if (args.move){ 604 | moveNodes(args.positionDiff, args.nodes); 605 | cy.elements().unselect(); 606 | } 607 | 608 | return args; 609 | }, 610 | _undo: function (args) { 611 | var diff = { 612 | x: -1 * args.positionDiff.x, 613 | y: -1 * args.positionDiff.y 614 | }; 615 | var result = { 616 | positionDiff: args.positionDiff, 617 | nodes: args.nodes, 618 | move: true 619 | }; 620 | moveNodes(diff, args.nodes); 621 | cy.elements().unselect(); 622 | return result; 623 | } 624 | }, 625 | "layout": { 626 | _do: function (args) { 627 | if (args.firstTime){ 628 | var positions = getNodePositions(); 629 | var layout; 630 | if(args.eles) { 631 | layout = getEles(args.eles).layout(args.options); 632 | } 633 | else { 634 | layout = cy.layout(args.options); 635 | } 636 | 637 | // Do this check for cytoscape.js backward compatibility 638 | if (layout && layout.run) { 639 | layout.run(); 640 | } 641 | 642 | return positions; 643 | } else 644 | return returnToPositions(args); 645 | }, 646 | _undo: function (nodesData) { 647 | return returnToPositions(nodesData); 648 | } 649 | }, 650 | "changeParent": { 651 | _do: function (args) { 652 | return (cy.nodes()[0].component ? changeParentNew(args) : changeParentOld(args)); 653 | }, 654 | _undo: function (args) { 655 | return (cy.nodes()[0].component ? changeParentNew(args) : changeParentOld(args)); 656 | } 657 | }, 658 | "batch": { 659 | _do: function (args) { 660 | return batch(args, "do"); 661 | }, 662 | _undo: function (args) { 663 | return batch(args, "undo"); 664 | } 665 | } 666 | }; 667 | } 668 | 669 | }; 670 | 671 | if (typeof module !== 'undefined' && module.exports) { // expose as a commonjs module 672 | module.exports = register; 673 | } 674 | 675 | if (typeof define !== 'undefined' && define.amd) { // expose as an amd/requirejs module 676 | define('cytoscape.js-undo-redo', function () { 677 | return register; 678 | }); 679 | } 680 | 681 | if (typeof cytoscape !== 'undefined') { // expose to global cytoscape (i.e. window.cytoscape) 682 | register(cytoscape); 683 | } 684 | 685 | })(); 686 | --------------------------------------------------------------------------------