├── .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 |
--------------------------------------------------------------------------------