├── .gitmodules ├── .jshintrc ├── examples ├── samplecode.txt ├── sounds │ ├── glass.mp3 │ ├── glass.ogg │ ├── beer_can_opening.mp3 │ ├── beer_can_opening.ogg │ └── sounds.html ├── manual-graph.html ├── slideshow.html ├── binarytree.html ├── findmax.html ├── sudoku.html ├── keyvaluepair.html ├── list.html ├── pseudocode.html ├── graph.html ├── simple-exercise.html ├── allStructures.html └── pointers.html ├── css └── images │ ├── spinner.gif │ ├── settings.png │ ├── sound-off.png │ └── sound-icon.png ├── src ├── front1.txt ├── version2.txt ├── front2.txt ├── version1.txt ├── events.js ├── translations.js ├── core.js ├── messages.js ├── matrix.js ├── settings.js ├── keyvaluepair.js ├── questions.js ├── effects.js └── anim.js ├── .travis.yml ├── extras ├── stack.css ├── sound.js ├── lib │ └── ion.sound.js └── stack.js ├── .gitignore ├── test ├── utils │ ├── arrayutils.js │ ├── qunit-assert-close.js │ └── qunit.css ├── unit │ ├── core.js │ ├── keyvaluepair.js │ ├── exercise.js │ ├── utils.js │ ├── anim.js │ ├── graph.js │ └── list.js └── index.html ├── MIT-license.txt ├── package.json ├── Makefile ├── README.md ├── Gruntfile.js └── Changelog.txt /.gitmodules: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vkaravir/JSAV/HEAD/.jshintrc -------------------------------------------------------------------------------- /examples/samplecode.txt: -------------------------------------------------------------------------------- 1 | line1 from a URL 2 | line2 3 | line 3 -------------------------------------------------------------------------------- /css/images/spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vkaravir/JSAV/HEAD/css/images/spinner.gif -------------------------------------------------------------------------------- /src/front1.txt: -------------------------------------------------------------------------------- 1 | /*! 2 | * JSAV - JavaScript Algorithm Visualization Library 3 | * Version -------------------------------------------------------------------------------- /css/images/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vkaravir/JSAV/HEAD/css/images/settings.png -------------------------------------------------------------------------------- /css/images/sound-off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vkaravir/JSAV/HEAD/css/images/sound-off.png -------------------------------------------------------------------------------- /css/images/sound-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vkaravir/JSAV/HEAD/css/images/sound-icon.png -------------------------------------------------------------------------------- /examples/sounds/glass.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vkaravir/JSAV/HEAD/examples/sounds/glass.mp3 -------------------------------------------------------------------------------- /examples/sounds/glass.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vkaravir/JSAV/HEAD/examples/sounds/glass.ogg -------------------------------------------------------------------------------- /src/version2.txt: -------------------------------------------------------------------------------- 1 | "; 2 | 3 | JSAV.version = function() { 4 | return theVERSION; 5 | }; 6 | })(); 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | before_script: 5 | - npm install -g grunt-cli 6 | -------------------------------------------------------------------------------- /examples/sounds/beer_can_opening.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vkaravir/JSAV/HEAD/examples/sounds/beer_can_opening.mp3 -------------------------------------------------------------------------------- /examples/sounds/beer_can_opening.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vkaravir/JSAV/HEAD/examples/sounds/beer_can_opening.ogg -------------------------------------------------------------------------------- /src/front2.txt: -------------------------------------------------------------------------------- 1 | 2 | * Copyright (c) 2011-2015 by Ville Karavirta and Cliff Shaffer 3 | * Released under the MIT license. 4 | */ 5 | -------------------------------------------------------------------------------- /src/version1.txt: -------------------------------------------------------------------------------- 1 | /** 2 | * Version support 3 | * Depends on core.js 4 | */ 5 | (function() { 6 | if (typeof JSAV === "undefined") { return; } 7 | var theVERSION = " -------------------------------------------------------------------------------- /extras/stack.css: -------------------------------------------------------------------------------- 1 | .jsavstack { 2 | position: relative; 3 | background-color: inherit; 4 | } 5 | .jsavstacknode { 6 | position: absolute; 7 | border-radius: 5px; 8 | width: 45px; 9 | height: 45px; 10 | z-index: 100; 11 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #Temp files 2 | *~ 3 | #Mac stuff 4 | .DS_Store 5 | 6 | #build directory 7 | build 8 | 9 | #Eclipse settings 10 | .project 11 | .settings 12 | 13 | #InteliJ IDE settings 14 | .idea 15 | 16 | #Generated files 17 | src/version.txt 18 | src/front.js 19 | src/version.js 20 | node_modules 21 | -------------------------------------------------------------------------------- /extras/sound.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | "use strict"; 3 | JSAV.ext.sound = JSAV.anim(function(filename, options) { 4 | $.ionSound.addSound(filename); 5 | 6 | if (this._soundPlaying) { // stop previous sound 7 | // this._soundPlaying is never cleared; we can still stop it 8 | $.ionSound.stop(this._soundPlaying); 9 | } 10 | 11 | // start the new sound 12 | if (this._shouldAnimate()) { 13 | this._soundPlaying = filename; 14 | $.ionSound.play(filename); 15 | } 16 | return [filename, options]; 17 | }); 18 | 19 | // register function to be called when JSAV is initialized 20 | JSAV.init(function(options) { 21 | // initialize ionSound on JSAV initialization 22 | $.ionSound({sounds: [], path: options.soundPath || "./" }); 23 | }); 24 | }(jQuery)); -------------------------------------------------------------------------------- /test/utils/arrayutils.js: -------------------------------------------------------------------------------- 1 | var arrayUtils = { 2 | 3 | testArrayHighlights: function(arr, hlIndices) { 4 | for (var i = 0, l = arr.size(); i < l; i++) { 5 | // test highlights through array.isHighlight 6 | equal(arr.isHighlight(i), hlIndices[i]); 7 | // test highlights through the corresponding index objects 8 | equal(arr.index(i).isHighlight(), hlIndices[i]); 9 | } 10 | }, 11 | 12 | testArrayValues: function(arr, values) { 13 | equal(arr.size(), values.length, "Equal array sizes"); 14 | for (var i = 0, l = values.length; i < l; i++) { 15 | // test values through array 16 | equal(arr.value(i), values[i], "Values in index " + i); 17 | // test values through the corresponding index objects 18 | equal(arr.index(i).value(), values[i]); 19 | } 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /MIT-license.txt: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2011- Ville Karavirta and Cliff Shaffer 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "JSAV", 3 | "version": "1.0.1", 4 | "description": "JSAV - The JavaScript Algorithm Visualization Library", 5 | "homepage": "http://jsav.io/", 6 | "license": "MIT", 7 | "author": "Ville Karavirta ", 8 | "contributors": [ 9 | { 10 | "name": "Cliff Shaffer" 11 | }, 12 | { 13 | "name": "Ville Karavirta", 14 | "email": "ville@villekaravirta.com" 15 | }, 16 | { 17 | "name": "Matthew Micallef", 18 | "email": "mattmicallef@outlook.com" 19 | } 20 | ], 21 | "directories": { 22 | "example": "Example usages of JSAV", 23 | "test": "Unittests", 24 | "src": "The JS sourcefiles" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/vkaravir/JSAV.git" 29 | }, 30 | "bugs": { 31 | "url": "https://github.com/vkaravir/JSAV/issues" 32 | }, 33 | "scripts": { 34 | "test": "grunt test" 35 | }, 36 | "devDependencies": { 37 | "grunt": "^1.0.1", 38 | "grunt-contrib-concat": "^1.0.1", 39 | "grunt-contrib-copy": "^1.0.0", 40 | "grunt-contrib-csslint": "^2.0.0", 41 | "grunt-contrib-jshint": "^1.1.0", 42 | "grunt-contrib-qunit": "^2.0.0", 43 | "grunt-contrib-uglify": "^3.2.1", 44 | "grunt-contrib-watch": "^1.0.0", 45 | "grunt-exec": "^3.0.0", 46 | "grunt-gitinfo": "^0.1.8", 47 | "load-grunt-tasks": "^3.5.2" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /examples/sounds/sounds.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Example of Sounds in JSAV 5 | 6 | 17 | 18 | 19 |

Example of All Data Structures in JSAV

20 |

Note: the sounds used are part of the Ion.Sound jQuery plugin and are licensed under the MIT license.

21 |
22 |

23 |

24 |

25 |
26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /test/unit/core.js: -------------------------------------------------------------------------------- 1 | /*global ok,test,module,deepEqual,equal,expect,equals,notEqual */ 2 | module("core", { }); 3 | 4 | test("JSAV", function() { 5 | expect(4); 6 | ok( JSAV, "JSAV" ); 7 | ok( JSAV.ext, "JSAV extensions"); 8 | ok( JSAV.init, "JSAV init"); 9 | var av = new JSAV("emptycontainer"); 10 | ok( av, "JSAV initialized" ); 11 | }); 12 | 13 | test("JSAV container options", function() { 14 | var avDOM = new JSAV(document.getElementById("emptycontainer")); 15 | ok(avDOM, "Passing a DOM element"); 16 | 17 | var avjQuery = new JSAV($("#emptycontainer")); 18 | ok(avjQuery, "Passing a jQuery object"); 19 | 20 | var avStringId = new JSAV("emptycontainer"); 21 | ok(avStringId, "Passing an element id"); 22 | 23 | var avDOMOpt = new JSAV({element: document.getElementById("emptycontainer")}); 24 | ok(avDOMOpt, "Passing a DOM element as an option"); 25 | 26 | var avjQueryOpt = new JSAV({element: jQuery("#emptycontainer")}); 27 | ok(avjQueryOpt, "Passing a jQuery object as an option"); 28 | 29 | var avSelectorOpt = new JSAV({element: "#emptycontainer"}); 30 | ok(avSelectorOpt, "Passing a selector as an option"); 31 | 32 | window.JSAV_OPTIONS = {element: "#emptycontainer"}; 33 | var avSelectorGlobalOpt = new JSAV(); 34 | ok(avSelectorGlobalOpt, "Element as a selector in global JSAV_OPTION"); 35 | delete window.JSAV_OPTIONS; 36 | }); 37 | 38 | test("JSAV Options", function() { 39 | // simple test to see if global JSAV_OPTIONS works 40 | window.JSAV_OPTIONS = {cat: 0, dog: 1}; 41 | var av = new JSAV("emptycontainer", {cat: 3, turtle: 42}); 42 | equal(av.options.turtle, 42, "Basic option preserved"); 43 | equal(av.options.dog, 1, "Global option"); 44 | equal(av.options.cat, 3, "Global option also specified on initialization"); 45 | // delete the global variable 46 | delete window.JSAV_OPTIONS; 47 | }); -------------------------------------------------------------------------------- /examples/manual-graph.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Depth-First Search 5 | 6 | 7 | 8 |

JSAV example for graph with manual layout

9 |
10 |
11 |

12 |
13 | 14 | 15 | 16 | 17 | 18 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | RM = rm -rf 2 | LIB = lib 3 | MINIMIZE = uglifyjs $(TARGET)/JSAV.js --comments '/^!|@preserve|@license|@cc_on/i' >$(TARGET)/JSAV-min.js 4 | CAT = cat 5 | SRC = src 6 | TARGET = build 7 | 8 | SOURCES = $(SRC)/front.js $(SRC)/core.js $(SRC)/translations.js $(SRC)/anim.js $(SRC)/utils.js $(SRC)/messages.js $(SRC)/effects.js $(SRC)/events.js $(SRC)/graphicals.js $(SRC)/datastructures.js $(SRC)/array.js $(SRC)/tree.js $(SRC)/list.js $(SRC)/graph.js $(SRC)/matrix.js $(SRC)/code.js $(SRC)/settings.js $(SRC)/questions.js $(SRC)/exercise.js $(SRC)/version.js 9 | 10 | # This only works right when this is a submodule 11 | FETCH = ../.git/modules/JSAV/FETCH_HEAD 12 | 13 | all: build 14 | 15 | clean: 16 | $(RM) *~ 17 | $(RM) build/* 18 | $(RM) examples/*~ 19 | $(RM) src/*~ src/version.txt src/front.js src/version.js 20 | $(RM) css/*~ 21 | 22 | build: $(TARGET)/JSAV.js $(TARGET)/JSAV-min.js 23 | 24 | $(TARGET)/JSAV.js: $(SRC)/version.txt $(SRC)/front.js $(SRC)/version.js $(SOURCES) 25 | -mkdir $(TARGET) 26 | $(CAT) $(SOURCES) > $(TARGET)/JSAV.js 27 | 28 | $(FETCH): 29 | 30 | $(SRC)/version.txt: $(FETCH) 31 | git describe --tags --long | awk '{ printf "%s", $$0 }' - > $(SRC)/version.txt 32 | 33 | $(SRC)/front.js: $(SRC)/front1.txt $(SRC)/version.txt $(SRC)/front2.txt 34 | $(CAT) $(SRC)/front1.txt $(SRC)/version.txt $(SRC)/front2.txt > $(SRC)/front.js 35 | 36 | $(SRC)/version.js :$(SRC)/version1.txt $(SRC)/version.txt $(SRC)/version2.txt 37 | $(CAT) $(SRC)/version1.txt $(SRC)/version.txt $(SRC)/version2.txt > $(SRC)/version.js 38 | 39 | $(TARGET)/JSAV-min.js: $(SRC)/version.txt $(SRC)/front.js $(SRC)/version.js $(SOURCES) 40 | -$(MINIMIZE) 41 | 42 | jshint: 43 | -jshint src/ 44 | 45 | csslint: 46 | csslint --errors=empty-rules,import,errors --warnings=duplicate-background-images,compatible-vendor-prefixes,display-property-grouping,fallback-colors,duplicate-properties,shorthand,gradients,font-sizes,floats,overqualified-elements,import,regex-selectors,rules-count,unqualified-attributes,vendor-prefix,zero-units css/JSAV.css 47 | 48 | lint: jshint csslint -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #JSAV [![Travis CI Build Status](https://travis-ci.org/vkaravir/JSAV.svg?branch=master)](https://travis-ci.org/vkaravir/JSAV) 2 | This is the JSAV development library for creating Algorithm 3 | Visualizations in JavaScript. 4 | 5 | JSAV is a part of the [OpenDSA](https://github.com/OpenDSA/OpenDSA/) project. OpenDSA aims to create a 6 | complete hypertextbook for Data Structures and Algorithms along with 7 | the necessary supporting infrastructure. For more information about 8 | OpenDSA, see http://algoviz.org/ebook . 9 | 10 | ## License 11 | 12 | JSAV and OpenDSA are released under the MIT license. See the file 13 | MIT-license.txt included with this distribution. 14 | 15 | ## Documentation 16 | The JSAV documentation is available at [jsav.io](http://jsav.io/) 17 | 18 | ## Extensions 19 | JSAV is extandible, meaning that you can create your own data structures 20 | for it or use data structures created by someone else. OpenDSA contains 21 | several extensions which can be found 22 | [here](https://github.com/OpenDSA/OpenDSA/tree/master/DataStructures). 23 | 24 | ## For developers 25 | 26 | The day-to-day working JSAV repository is located at GitHub. For new 27 | developers who want to use the Github working version of JSAV: 28 | 29 | * Install Git 30 | * Check out the JSAV repository. For example, at the commandline you 31 | can do the following to create a new JSAV folder or directory: 32 | git clone git://github.com/vkaravir/JSAV.git JSAV 33 | (Note that this is a read-only URL. If you are joining the developer 34 | team, and you are not sufficiently familiar with Git to know what 35 | to do to set things up right to be able to push changes, talk to us 36 | about it.) 37 | * Go to the JSAV folder or directory that you just created and run: 38 | make 39 | This will "compile" the pieces together for you. At this point, you 40 | are ready to try out the examples or invoke your copy of JSAV in 41 | your own development projects. 42 | 43 | For SVN users new to git: 44 | 45 | * To "checkout" a new copy of the library, use "git clone". 46 | * To "update" your copy of the repository, use "git pull". 47 | -------------------------------------------------------------------------------- /examples/slideshow.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | JSAV Slideshow Demonstration 5 | 6 | 17 | 18 | 19 |

JSAV Slideshow Demonstration

20 |
21 |
22 |

23 |
24 |
25 | 27 | 28 | 29 | 30 | 31 | 32 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /test/utils/qunit-assert-close.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2013 jQuery Foundation and other contributors 3 | http://jquery.com/ 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | */ 25 | QUnit.extend(QUnit.assert, { 26 | /** 27 | * Checks that the first two arguments are equal, or are numbers close enough to be considered equal 28 | * based on a specified maximum allowable difference. 29 | * 30 | * @example assert.close(3.141, Math.PI, 0.001); 31 | * 32 | * @param Number actual 33 | * @param Number expected 34 | * @param Number maxDifference (the maximum inclusive difference allowed between the actual and expected numbers) 35 | * @param String message (optional) 36 | */ 37 | close: function(actual, expected, maxDifference, message) { 38 | var passes = (actual === expected) || Math.abs(actual - expected) <= maxDifference; 39 | QUnit.push(passes, actual, expected, message); 40 | }, 41 | 42 | /** 43 | * Checks that the first two arguments are numbers with differences greater than the specified 44 | * minimum difference. 45 | * 46 | * @example assert.notClose(3.1, Math.PI, 0.001); 47 | * 48 | * @param Number actual 49 | * @param Number expected 50 | * @param Number minDifference (the minimum exclusive difference allowed between the actual and expected numbers) 51 | * @param String message (optional) 52 | */ 53 | notClose: function(actual, expected, minDifference, message) { 54 | QUnit.push(Math.abs(actual - expected) > minDifference, actual, expected, message); 55 | } 56 | }); 57 | -------------------------------------------------------------------------------- /examples/binarytree.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Example of Binary Tree 5 | 6 | 27 | 28 | 29 |

JSAV slideshow for binary trees

30 |
31 |
32 |
33 | 34 | 35 | 36 | 37 | 38 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /examples/findmax.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Findmax 5 | 7 | 8 | 9 |

Selecting the maximum

10 |

This is demonstration of supporting interactivity outside the 11 | context of a slideshow or standard proficiency exercise. 12 | (An example of a profiency exercise is shown in 13 | simple-exercise.html in this directory.) 14 | This might be useful, for example, in 15 | connection with using JSAV within Khan Academy exercises.

16 | 17 |

First we create and display a JSAV array with some random 18 | numbers. You can click on values in the array to highlight or 19 | unhighlight them. Your goal is to highlight (only) the array 20 | position with the biggest element.

21 | 22 |
23 |
24 |

25 |
26 | 27 | 28 | 29 | 30 | 31 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /test/unit/keyvaluepair.js: -------------------------------------------------------------------------------- 1 | /*global ok,test,module,deepEqual,equal,expect,notEqual,arrayUtils */ 2 | (function() { 3 | "use strict"; 4 | module("datastructures.keyvaluepair", { }); 5 | test("Key-Value Pair", function() { 6 | var av = new JSAV("emptycontainer"); 7 | ok( av, "JSAV initialized" ); 8 | ok( JSAV._types.ds.AVKeyValuePair, "KeyValue pair exists" ); 9 | var keyvaluepair = av.ds.keyValuePair({key: "hello", values: "1, 1"}); 10 | ok(keyvaluepair); 11 | }); 12 | 13 | test("Highlighting Key-Value Pair", function() { 14 | var av = new JSAV("emptycontainer"), 15 | keyvaluepair = av.ds.keyValuePair({key: "hello", values: "1, 1"}); 16 | keyvaluepair.highlight(); 17 | av.step(); 18 | keyvaluepair.unhighlight(); 19 | av.step(); 20 | keyvaluepair.highlightKey(); 21 | av.step(); 22 | keyvaluepair.unhighlightKey(); 23 | av.step(); 24 | keyvaluepair.highlightValues(); 25 | av.step(); 26 | keyvaluepair.unhighlightValues(); 27 | av.step(); 28 | av.recorded(); 29 | $.fx.off = true; 30 | 31 | equal(keyvaluepair.isHighlight(), false); 32 | 33 | av.forward(); 34 | 35 | equal(keyvaluepair.isHighlight(), true); 36 | 37 | av.forward(); 38 | 39 | equal(keyvaluepair.isHighlight(), false); 40 | 41 | av.forward(); 42 | 43 | equal(document.getElementsByClassName("jsav-pair-key-highlight").length, 1); 44 | 45 | av.forward(); 46 | 47 | equal(document.getElementsByClassName("jsav-pair-key-highlight").length, 0); 48 | 49 | av.forward(); 50 | 51 | equal(document.getElementsByClassName("jsav-pair-values-highlight").length, 1); 52 | 53 | av.forward(); 54 | 55 | equal(document.getElementsByClassName("jsav-pair-values-highlight").length, 0); 56 | }); 57 | 58 | test("ID Container for Key-Value Pair", function() { 59 | var av = new JSAV("emptycontainer"), 60 | keyvaluepair = av.ds.keyValuePair({key: "hello", values: "1, 1"}); 61 | 62 | keyvaluepair.addIDContainer("Test", 1); 63 | av.step(); 64 | av.recorded(); 65 | 66 | equal(document.getElementsByClassName("TestId").length, 1); 67 | }); 68 | 69 | test("Pair Equality for Key-Value Pair", function() { 70 | var av = new JSAV("emptycontainer"), 71 | keyvaluepair1 = av.ds.keyValuePair({key: "hello", values: "1, 1"}), 72 | keyvaluepair2 = av.ds.keyValuePair({key: "hello", values: "1, 1"}); 73 | 74 | var isEqual = keyvaluepair1.equals(keyvaluepair2); 75 | av.step(); 76 | av.recorded(); 77 | 78 | equal(isEqual, true); 79 | }); 80 | })(); -------------------------------------------------------------------------------- /examples/sudoku.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Sudoku 5 | 6 | 17 | 18 | 19 |

JSAV Sudoku Grid

20 |

This is a simple example of the JSAV matrix. 21 | It also illustrated updating slideshows interactively, 22 | on-the-fly. 23 | The example creates a sudoku game using a JSAV matrix. 24 | If you click on cells, they highlight the first time you click. 25 | Each click adds a new slide, which you can see by resetting the 26 | slideshow with the << control. 27 | View the source to see how this is done in CSS and JavaScript. 28 |

29 |
30 |
31 |

32 |
33 |
34 |
35 | 43 | 44 | 45 | 46 | 47 | 48 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /test/unit/exercise.js: -------------------------------------------------------------------------------- 1 | /*global JSAV,ok,test,module,strictEqual,deepEqual,equal,notEqual */ 2 | (function() { 3 | "use strict"; 4 | 5 | // The setup for an exercise which uses two arrays. The model answer 6 | // and the generated student answer differ in all but the first step. 7 | // In total there are four steps, and the differences are: 8 | // step 2: different indices are highlighted 9 | // step 3: different font-size is set for indices 10 | // step 4: a value in the array is set in the model solution 11 | // This setup can be used to test the compare option. 12 | function setupExercise(jsav, options) { 13 | var arr1, arr2; 14 | var init = function() { 15 | arr1 = jsav.ds.array([0, 1, 2, 3]); 16 | arr2 = jsav.ds.array([0, 1, 3, 2]); 17 | jsav.displayInit(); 18 | return [arr1, arr2]; 19 | }; 20 | var model = function(modeljsav) { 21 | var arr1 = modeljsav.ds.array([0, 1, 2, 3]), 22 | arr2 = modeljsav.ds.array([0, 1, 3, 2]); 23 | modeljsav.displayInit(); 24 | arr1.swap(1, 3); 25 | modeljsav.gradeableStep(); 26 | arr1.highlight(0); 27 | modeljsav.gradeableStep(); 28 | arr2.css(2, {color: "blue", fontSize: "20px"}); 29 | modeljsav.gradeableStep(); 30 | arr2.value(3, 3); 31 | modeljsav.gradeableStep(); 32 | return [arr1, arr2]; 33 | }; 34 | var exercise = jsav.exercise(model, init, options); 35 | exercise.reset(); 36 | 37 | arr1.swap(1, 3); 38 | jsav.gradeableStep(); 39 | arr1.highlight(1); 40 | jsav.gradeableStep(); 41 | arr2.css(2, {color: "blue", fontSize: "21px"}); 42 | jsav.gradeableStep(); 43 | return exercise; 44 | } 45 | 46 | module("exercise.grading", { }); 47 | test("Exercise compare option (multiple structures)", function() { 48 | var jsav = new JSAV("emptycontainer"); 49 | jsav.recorded(); 50 | jsav.SPEED = 0; 51 | var exer = setupExercise(jsav, {feedback: "atend"}); 52 | // test that the grading works properly and gives 3 correct when only values are compared 53 | strictEqual(exer.grade().correct, 3); 54 | strictEqual(exer.grade().total, 4); // just make sure the total step count matches 55 | 56 | // change the comparison to include fontSize 57 | exer.options.compare = [{}, {css: ["color", "fontSize"]}]; 58 | strictEqual(exer.grade().correct, 2); // only two first steps are now correct 59 | 60 | // compare classes and the jsavhighlight class 61 | exer.options.compare = [{class: "jsavhighlight"}, {}]; 62 | strictEqual(exer.grade().correct, 1); // only first step is now correct 63 | 64 | // incorrect compare option (should be an array), so expect only compare values 65 | exer.options.compare = {css: ["fontSize"]}; 66 | strictEqual(exer.grade().correct, 3); // three steps correct based on values 67 | }); 68 | }()); -------------------------------------------------------------------------------- /examples/keyvaluepair.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 | 9 | 10 |

Key-Value Pair Prototype

11 |
12 |
13 | 14 |

15 |
16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 80 | 81 | -------------------------------------------------------------------------------- /examples/list.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Linked List Example 6 | 7 | 18 | 19 | 20 |

JSAV slideshow for lists

21 |
22 |
23 |

24 |
25 |
26 | 27 | 28 | 29 | 30 | 31 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | // Grunt build file for JSAV 2 | module.exports = function(grunt) { 3 | "use strict"; 4 | 5 | var BUILD_DIR = 'build/', 6 | SRC_DIR = 'src/'; 7 | 8 | // autoload all grunt tasks in package.json 9 | require('load-grunt-tasks')(grunt); 10 | 11 | grunt.initConfig({ 12 | gitinfo: { // get the JSAV version info from git tag and number 13 | commands: { // expose it as gitinfo.version 14 | version: ['describe', '--tags', '--long'] 15 | } 16 | }, 17 | concat: { // concat the JSAV javascript file 18 | //options: { // use the header and footer specified above 19 | // banner: JS_BANNER, 20 | // footer: JS_FOOTER 21 | //}, 22 | build: { // the order matters, so list every file manually 23 | src: [SRC_DIR + 'front.js', 24 | SRC_DIR + 'core.js', 25 | SRC_DIR + 'translations.js', 26 | SRC_DIR + 'anim.js', 27 | SRC_DIR + 'utils.js', 28 | SRC_DIR + 'messages.js', 29 | SRC_DIR + 'effects.js', 30 | SRC_DIR + 'events.js', 31 | SRC_DIR + 'graphicals.js', 32 | SRC_DIR + 'datastructures.js', 33 | SRC_DIR + 'array.js', 34 | SRC_DIR + 'keyvaluepair.js', 35 | SRC_DIR + 'tree.js', 36 | SRC_DIR + 'list.js', 37 | SRC_DIR + 'graph.js', 38 | SRC_DIR + 'matrix.js', 39 | SRC_DIR + 'code.js', 40 | SRC_DIR + 'settings.js', 41 | SRC_DIR + 'questions.js', 42 | SRC_DIR + 'exercise.js', 43 | SRC_DIR + 'version.js'], 44 | dest: BUILD_DIR + 'JSAV.js' 45 | } 46 | }, 47 | uglify: { // for building the minified version 48 | build: { 49 | options: { 50 | preserveComments: 'some', 51 | mangle: false 52 | }, 53 | files: { 54 | 'build/JSAV-min.js': [BUILD_DIR + 'JSAV.js'] 55 | } 56 | } 57 | }, 58 | qunit: { 59 | files: ['test/index.html'] 60 | }, 61 | jshint: { // for linting the JS 62 | sources: ['Gruntfile.js', 'src/*.js'], 63 | tests: ['test/**/*.js'] 64 | }, 65 | csslint: { // for linting the CSS 66 | jsav: { 67 | src: ['css/JSAV.css'] 68 | } 69 | }, 70 | exec: { 71 | version: "git describe --tags --long | awk '{ printf \"%s\", $0 }' > " + SRC_DIR + "version.txt", 72 | front: "cat src/front1.txt src/version.txt src/front2.txt > src/front.js", 73 | versionjs: "cat src/version1.txt src/version.txt src/version2.txt > src/version.js" 74 | }, 75 | watch: { 76 | jssrc: { 77 | files: ['src/*.js'], 78 | tasks: ['default'] 79 | } 80 | } 81 | }); 82 | 83 | grunt.registerTask('build', ['exec', 'concat', 'uglify']); 84 | grunt.registerTask('lint', ['jshint', 'csslint']); 85 | grunt.registerTask('test', ['qunit']); 86 | grunt.registerTask('default', ['build']); 87 | }; -------------------------------------------------------------------------------- /examples/pseudocode.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | JSAV Example of Pseudocode Objects 5 | 6 | 16 | 17 | 18 |

JSAV Pseudocode Demonstration

19 |
20 |
21 |

22 |
23 | 24 | 25 | 26 | 27 | 28 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSAV Test Suite 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 |

JSAV Test Suite

56 |

57 |
58 |

59 |
    60 | 61 |
    62 |
    63 |
    64 |
    65 |

    66 |
      67 |
    1. 12
    2. 68 |
    3. 22
    4. 69 |
    5. 14
    6. 70 |
    7. 39
    8. 71 |
    9. false
    10. 72 |
    73 |
      74 |
    1. 6
    2. 75 |
    3. 4
    4. 76 |
    5. 77 |
    78 |
    79 | 80 |
    81 |
    82 | 83 | 84 | -------------------------------------------------------------------------------- /test/unit/utils.js: -------------------------------------------------------------------------------- 1 | /*global ok,test,module,deepEqual,equal,expect,equals,notEqual,strictEqual */ 2 | (function() { 3 | module("jsav.utils.rand", { }); 4 | test("seeding random", function() { 5 | var r = JSAV.utils.rand, 6 | seed = new Date().getTime(); 7 | r.seedrandom(seed); 8 | var rand1 = r.random(), 9 | rand2 = r.random(); 10 | r.seedrandom(seed); 11 | equal(r.random(), rand1); 12 | equal(r.random(), rand2); 13 | 14 | // ..different seed, different numbers 15 | r.seedrandom(seed + 1); 16 | notEqual(r.random(), rand1); 17 | notEqual(r.random(), rand2); 18 | }); 19 | test("numKey(s)", function() { 20 | var r = JSAV.utils.rand, 21 | seed = new Date().getTime(), 22 | fixedseed = "satunnaisuutta"; 23 | // min inclusive, max exclusive 24 | equal(r.numKey(4, 5), 4); 25 | 26 | r.seedrandom(fixedseed); 27 | var res = [31, 94, 49, 8, 26, 90, 9, 45, 78, 89]; 28 | for (var i= 0; i < res.length; i++) { 29 | equal(r.numKey(1, 99), res[i]); 30 | } 31 | r.seedrandom(fixedseed); 32 | deepEqual(r.numKeys(1, 99, 10), res); 33 | 34 | // empty array 35 | deepEqual(r.numKeys(1, 2, 0), []); 36 | // min inclusive, max exclusive 37 | deepEqual(r.numKeys(1, 2, 1), [1]); 38 | deepEqual(r.numKeys(1, 2, 10), [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]); 39 | // default sorting 40 | r.seedrandom(fixedseed); 41 | deepEqual(r.numKeys(1, 99, 10, {sorted: true}), [8, 9, 26, 31, 45, 49, 78, 89, 90, 94]); 42 | // custom sorting, same as default 43 | r.seedrandom(fixedseed); 44 | deepEqual(r.numKeys(1, 99, 10, {sorted: true, sortfunc: function(a, b) {return a - b;}}), 45 | [8, 9, 26, 31, 45, 49, 78, 89, 90, 94]); 46 | // custom sorting, desc order 47 | r.seedrandom(fixedseed); 48 | deepEqual(r.numKeys(1, 99, 10, {sorted: true, sortfunc: function(a, b) {return b - a;}}), 49 | [94, 90, 89, 78, 49, 45, 31, 26, 9, 8]); 50 | }); 51 | test("sample", function() { 52 | var r = JSAV.utils.rand, 53 | seed = new Date().getTime(), 54 | fixedseed = "satunnaisuutta", 55 | values = [31, 94, 49, 8, 26, 90, 9, 45, 78, 89]; 56 | 57 | equal(r.sample([], 1), undefined, "Sampling more than there are items should return undefined"); 58 | equal(r.sample([2, 4], 3), undefined); 59 | equal(r.sample([2, 4], -3), undefined, "Negative values should return undefined"); 60 | deepEqual(r.sample([2, 5], 0), [], "Sampling 0 items should return an empty array"); 61 | 62 | r.seedrandom(fixedseed); 63 | var res = r.sample(values, 7); 64 | equal(res.length, 7); 65 | deepEqual(res, [8, 89, 90, 31, 49, 94, 9]); 66 | }); 67 | test("iterable array", function() { 68 | var arr = [9, 8, 7, 6], 69 | iter = JSAV.utils.iterable(arr); 70 | notEqual(arr, iter); 71 | for (var i = 0, l = arr.length; i < l; i++) { 72 | ok(iter.hasNext()); 73 | equal(arr[i], iter.next()); 74 | } 75 | ok(!iter.hasNext()); 76 | for (i = 0, l = arr.length; i < l; i++) { 77 | equal(arr[i], iter[i]); 78 | } 79 | // reset iterator 80 | iter.reset(); 81 | for (i = 0, l = arr.length; i < l; i++) { 82 | ok(iter.hasNext()); 83 | equal(arr[i], iter.next()); 84 | } 85 | ok(!iter.hasNext()); 86 | for (i = 0, l = arr.length; i < l; i++) { 87 | equal(arr[i], iter[i]); 88 | } 89 | }); 90 | })(); 91 | -------------------------------------------------------------------------------- /Changelog.txt: -------------------------------------------------------------------------------- 1 | Changes in dev version 2 | ---------------------- 3 | * new minor features: 4 | - pointer position can be fixed 5 | 6 | Changes in version 0.7 7 | ---------------------- 8 | * new features: 9 | - support for graph data structure 10 | - support for pointer object 11 | - support for 2D arrays (or, matrices) 12 | - added path graphical primitive 13 | - added support for relative positioning of objects 14 | * new minor features: 15 | - new functions add/remove/has/toggleClass() to data structures 16 | - added general move/copy/swap effects 17 | - new events jsav-init and jsav-recorded triggered 18 | - enabled global options for JSAV and JSAV exercises 19 | - easier to extend JSAV objects due to "proper" inheritance of types 20 | - data structures Stack and Red-Black-Tree added to extras 21 | - more unified API for all JSAV objects 22 | - score widget for exercises in continuous feedback mode 23 | * numerous bug fixes in grading, trees, bar array 24 | 25 | Changes in version 0.6 26 | ---------------------- 27 | * new features: 28 | - support for pseudocode view 29 | * numerous bug fixes 30 | * new minor features: 31 | - tree nodes now have a remove() function to remove that node 32 | - tree node show/hide now work recursively and hide the whole subtree 33 | - significant performance improvements in proficiency exercise grading 34 | - customizable showing of score and a score widget in proficiency exercises 35 | - enabled logging of student actions in JSAV visualizations 36 | 37 | Changes in version 0.5 38 | ---------------------- 39 | * new features: 40 | - support for linked list data structure 41 | * numerous small bug fixes 42 | * new minor features: 43 | - functions to access edges in a tree more easily 44 | - possibility to call JSAV animation functions without recording the change 45 | - support for gettings bounds() of a data structure 46 | - support for 2-step layout; 1) calculating bounds without layout change, 2) update layout 47 | 48 | Changes in version 0.4.3 49 | ------------------------ 50 | * new features: 51 | - absolute positioning of labels, variables and data structures is now possible 52 | - labels can be attached to edges 53 | * bug fixes: 54 | - issue with changing array value in horizontal layout in Webkit based browsers fixed 55 | - show/hide bug for data structures fixed 56 | * new minor features: 57 | - allow custom event binding for data structures 58 | - allow data to be passed to event handlers for data structures 59 | - added function JSAV.utils.rand.sample to get a random sample from an array 60 | 61 | Changes in version 0.4.2 62 | ------------------------ 63 | * Possibility to better control array styling 64 | * Fixed a bug in tree positioning 65 | * Added function isAnimating() to check whether an animation is playing 66 | * Restructured data structure implementations into modules: 67 | - array moved to array.js 68 | - layouts moved to same module as DS implementation 69 | * Added easier way to attach event handlers to the data structures 70 | * Fixed a bug in array and tree value handling 71 | 72 | Changes in version 0.4 73 | ---------------------- 74 | * Support for tree and binary tree 75 | * Support for vertical array layout 76 | * Removed the initial support for binary search tree 77 | * Documentation for graphical primitives 78 | * Styling improvements to array visualization 79 | * General swap effect added; swap now shows an arrow for the swapped elements 80 | * License changed to MIT license. 81 | 82 | For changes before version 0.3.1, see 83 | http://algoviz.org/algoviz-wiki/index.php/JSAV_Roadmap -------------------------------------------------------------------------------- /examples/graph.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Depth-First Search 5 | 6 | 32 | 33 | 34 |

    JSAV slideshow for graphs

    35 |
    36 |
    37 |

    38 |
    39 | 40 | 41 | 42 | 43 | 44 | 45 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /examples/simple-exercise.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Simple JSAV Proficiency Exercise Example 5 | 6 | 7 | 8 | 9 | 28 |
    29 |

    A Simple JSAV Interactive Exercise

    30 |

    31 | Settings 32 |

    33 |

    34 |

    35 |
    36 | 37 | 38 | 39 | 40 | 41 | 42 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /src/events.js: -------------------------------------------------------------------------------- 1 | /*global JSAV, jQuery */ 2 | (function($) { 3 | "use strict"; 4 | // events to register as functions on tree 5 | var events = ["click", "dblclick", "mousedown", "mousemove", "mouseup", 6 | "mouseenter", "mouseleave"]; 7 | // returns a function for the passed eventType that binds a passed 8 | // function to that eventType nodes/edges in the tree 9 | // eventOpts that can be specified: 10 | // - selector: specify a CSS selector that will select the element which the event listener is attached to 11 | // (essentially this is the selector passed to jQuery .on() function). for example .jsavnode 12 | // - dataField: the name of the data field where the JSAV object can be found. for example node 13 | // - logEventPrefix: the prefix used when logging this event. the name of the event (i.e click) will be 14 | // prepended to this prefix. for example jsav-node- 15 | var eventhandler = function(eventType, eventOpts) { 16 | return function(data, handler, options) { 17 | // default options; not enabled for edges by default 18 | var defaultopts = {edge: false}, 19 | jsav = this.jsav, 20 | opts = defaultopts; // by default, go with default options 21 | if (typeof options === "object") { // 3 arguments, last one is options 22 | opts = $.extend(defaultopts, options); 23 | } else if (typeof handler === "object") { // 2 arguments, 2nd is options 24 | opts = $.extend(defaultopts, handler); 25 | } 26 | if (!opts.edge || opts.node) { 27 | // bind an event handler for nodes in this tree 28 | this.element.on(eventType, eventOpts.selector, function(e) { 29 | var $curr = $(this), 30 | elem = $curr.data(eventOpts.dataField); // get the JSAV node object 31 | while (!elem) { 32 | $curr = $curr.parent(); 33 | elem = $curr.data(eventOpts.dataField); 34 | } 35 | jsav.logEvent({type: eventOpts.logEventPrefix + eventType, objid: elem.id(), objvalue: elem.value() }); 36 | if ($.isFunction(data)) { // if no data -> 1st arg is the handler function 37 | // bind this to the elem and call handler 38 | // with the event as parameter 39 | data.call(elem, e); 40 | } else if ($.isFunction(handler)) { // data provided, 2nd arg is the handler function 41 | var params = $.isArray(data)?data.slice(0):[data]; // get a cloned array or data as array 42 | params.push(e); // jQuery event as the last parameter 43 | handler.apply(elem, params); // apply the given handler function 44 | } 45 | }); 46 | } 47 | if (opts.edge) { // if supposed to attach the handler to edges 48 | // find the SVG elements matching this tree's container 49 | this.jsav.canvas.on(eventType, '.jsavedge[data-container="' + this.id() + '"]', function(e) { 50 | var edge = $(this).data("edge"); // get the JSAV edge object 51 | jsav.logEvent({type: "jsav-edge-" + eventType, startvalue: edge.start().value(), 52 | endvalue: edge.end().value(), startid: edge.start().id(), endid: edge.end().id() }); 53 | if ($.isFunction(data)) { // no data 54 | // bind this to the edge and call handler 55 | // with the event as parameter 56 | data.call(edge, e); 57 | } else if ($.isFunction(handler)) { // data provided 58 | var params = $.isArray(data)?data.slice(0):[data]; // get a cloned array or data as array 59 | params.push(e); // jQuery event as the last parameter 60 | handler.apply(edge, params); // apply the function 61 | } 62 | }); 63 | } 64 | return this; // enable chaining of calls 65 | }; 66 | }; 67 | var on = function(eventOpts) { 68 | return function(eventName, data, handler, options) { 69 | eventhandler(eventName, eventOpts).call(this, data, handler, options); 70 | return this; 71 | }; 72 | }; 73 | 74 | JSAV.utils._events = { 75 | _addEventSupport: function(proto, options) { 76 | var opts = $.extend({selector: ".jsavnode", logEventPrefix: "jsav-node-", dataField: "node"}, options); 77 | // create the event binding functions and add to the given prototype 78 | for (var i = events.length; i--; ) { 79 | proto[events[i]] = eventhandler(events[i], opts); 80 | } 81 | proto.on = on(opts); 82 | } 83 | }; 84 | }(jQuery)); -------------------------------------------------------------------------------- /test/unit/anim.js: -------------------------------------------------------------------------------- 1 | /*global ok,test,module,deepEqual,equal,expect,notEqual,arrayUtils */ 2 | (function() { 3 | "use strict"; 4 | module("anim", {}); 5 | 6 | test("slideshow counter", function() { 7 | var av = new JSAV("arraycontainer"), 8 | arr = av.ds.array($("#array")), 9 | i = 0, 10 | counter = $("#arraycontainer .jsavcounter"); 11 | arr.highlight(0); 12 | av.step(); 13 | arr.highlight(1); 14 | av.recorded(); // will rewind it 15 | // bind listener to test event firing as well 16 | av.container.bind("jsav-updatecounter", function(e) { i++; }); 17 | equal("1 / 3", counter.text(), "Testing counter text"); 18 | av.forward(); 19 | equal("2 / 3", counter.text(), "Testing counter text"); 20 | av.forward(); 21 | equal("3 / 3", counter.text(), "Testing counter text"); 22 | av.forward(); // does nothing, updatecounter does not fire 23 | equal("3 / 3", counter.text(), "Testing counter text"); 24 | av.begin(); // fires two events, one for each step forward 25 | equal("1 / 3", counter.text(), "Testing counter text"); 26 | av.end(); // fires two events, one for each step backward 27 | equal("3 / 3", counter.text(), "Testing counter text"); 28 | av.backward(); 29 | equal("2 / 3", counter.text(), "Testing counter text"); 30 | av.forward(); 31 | equal("3 / 3", counter.text(), "Testing counter text"); 32 | av.backward(); 33 | av.backward(); 34 | equal("1 / 3", counter.text(), "Testing counter text"); 35 | av.backward(); // does nothing, updatecounter does not fire 36 | equal("1 / 3", counter.text(), "Testing counter text"); 37 | equal(i, 10, "Number of updatecounter events fired"); 38 | }); 39 | 40 | test("animator control events", function() { 41 | var av = new JSAV("emptycontainer"), 42 | arr = av.ds.array([10, 20, 30, 40]), 43 | props = ["color", "background-color"]; 44 | arr.highlight(0); 45 | av.step(); 46 | arr.highlight(1); 47 | av.step(); 48 | arr.highlight(2); 49 | av.recorded(); // will rewind it 50 | jQuery.fx.off = true; // turn off smooth animation 51 | av.RECORD = true; // fake to not animate the properties 52 | arrayUtils.testArrayHighlights(arr, [0, 0, 0, 0], props); 53 | av.container.trigger("jsav-end"); // apply all highlights 54 | arrayUtils.testArrayHighlights(arr, [1, 1, 1, 0], props); 55 | av.container.trigger("jsav-begin"); // undo everything 56 | arrayUtils.testArrayHighlights(arr, [0, 0, 0, 0], props); 57 | jQuery.fx.off = true; // turn off smooth animation 58 | av.container.trigger("jsav-forward"); 59 | arrayUtils.testArrayHighlights(arr, [1, 0, 0, 0], props); 60 | av.container.trigger("jsav-forward"); // second highlight 61 | arrayUtils.testArrayHighlights(arr, [1, 1, 0, 0], props); 62 | av.container.trigger("jsav-backward"); // undo second highlight 63 | arrayUtils.testArrayHighlights(arr, [1, 0, 0, 0], props); 64 | }); 65 | 66 | test("backward/forward filters", function() { 67 | var av = new JSAV("emptycontainer"), 68 | arr = av.ds.array([10, 20, 30, 40]), 69 | props = ["color", "background-color"]; 70 | arr.highlight(0); 71 | av.stepOption("stop0", true); 72 | av.step(); 73 | arr.highlight(1); 74 | av.stepOption("stop1", true); 75 | av.step(); 76 | arr.highlight(2); 77 | av.step(); 78 | av.stepOption("stop2", true); 79 | arr.highlight(3); 80 | av.recorded(); // will rewind it 81 | arrayUtils.testArrayHighlights(arr, [0, 0, 0, 0], props); 82 | av.end(); 83 | arrayUtils.testArrayHighlights(arr, [1, 1, 1, 1], props); 84 | 85 | jQuery.fx.off = true; // turn off smooth animation 86 | av.backward(function(step) { return step.options.stop0; }); // should go to beginning 87 | equal(av.currentStep(), 0); 88 | arrayUtils.testArrayHighlights(arr, [0, 0, 0, 0], props); 89 | 90 | av.end(); 91 | av.backward(function(step) { return step.options.stop1; }); // should go to step 1 92 | arrayUtils.testArrayHighlights(arr, [1, 0, 0, 0], props); 93 | equal(av.currentStep(), 1); 94 | av.begin(); 95 | 96 | av.forward(function(step) { return step.options.stop2; }); // end 97 | arrayUtils.testArrayHighlights(arr, [1, 1, 1, 1], props); 98 | equal(av.currentStep(), 4); 99 | av.begin(); 100 | 101 | av.forward(function(step) { return step.options.stop1; }); // step 2 102 | arrayUtils.testArrayHighlights(arr, [1, 1, 0, 0], props); 103 | equal(av.currentStep(), 2); 104 | 105 | }); 106 | })(); -------------------------------------------------------------------------------- /examples/allStructures.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Example of All Data Structures in JSAV 5 | 6 | 33 | 34 | 35 |

    Example of All Data Structures in JSAV

    36 |
    37 |
    38 | 39 | 40 | 41 | 42 | 43 | 143 | 144 | 145 | -------------------------------------------------------------------------------- /extras/lib/ion.sound.js: -------------------------------------------------------------------------------- 1 | // Ion.Sound 2 | // version 1.3.0 Build: 20 3 | // © 2013 Denis Ineshin | IonDen.com 4 | // 5 | // Project page: http://ionden.com/a/plugins/ion.sound/en.html 6 | // GitHub page: https://github.com/IonDen/ion.sound 7 | // 8 | // Released under MIT licence: 9 | // http://ionden.com/a/plugins/licence-en.html 10 | // ===================================================================================================================== 11 | 12 | (function ($) { 13 | 14 | if ($.ionSound) { 15 | return; 16 | } 17 | 18 | 19 | var settings = {}, 20 | soundsNum, 21 | canMp3, 22 | url, 23 | i, 24 | 25 | sounds = {}, 26 | playing = false, 27 | 28 | VERSION = "1.3.0"; 29 | 30 | 31 | var createSound = function (soundInfo) { 32 | var name, 33 | volume; 34 | 35 | if (soundInfo.indexOf(":") !== -1) { 36 | name = soundInfo.split(":")[0]; 37 | volume = soundInfo.split(":")[1]; 38 | } else { 39 | name = soundInfo; 40 | } 41 | console.log(name, volume); 42 | sounds[name] = new Audio(); 43 | canMp3 = sounds[name].canPlayType("audio/mp3"); 44 | if (canMp3 === "probably" || canMp3 === "maybe") { 45 | url = settings.path + name + ".mp3"; 46 | } else { 47 | url = settings.path + name + ".ogg"; 48 | } 49 | console.log(url); 50 | $(sounds[name]).prop("src", url); 51 | sounds[name].load(); 52 | sounds[name].preload = "auto"; 53 | sounds[name].volume = volume || settings.volume; 54 | }; 55 | 56 | 57 | var playSound = function (info) { 58 | var $sound, 59 | name, 60 | volume, 61 | playing_int; 62 | 63 | if (info.indexOf(":") !== -1) { 64 | name = info.split(":")[0]; 65 | volume = info.split(":")[1]; 66 | } else { 67 | name = info; 68 | } 69 | 70 | $sound = sounds[name]; 71 | 72 | if (typeof $sound !== "object" || $sound === null) { 73 | return; 74 | } 75 | 76 | 77 | if (volume) { 78 | $sound.volume = volume; 79 | } 80 | 81 | if (!settings.multiPlay && !playing) { 82 | 83 | $sound.play(); 84 | playing = true; 85 | 86 | playing_int = setInterval(function () { 87 | if ($sound.ended) { 88 | clearInterval(playing_int); 89 | playing = false; 90 | } 91 | }, 250); 92 | 93 | } else if (settings.multiPlay) { 94 | 95 | if ($sound.ended) { 96 | $sound.play(); 97 | } else { 98 | try { 99 | $sound.currentTime = 0; 100 | } catch (e) {} 101 | $sound.play(); 102 | } 103 | 104 | } 105 | }; 106 | 107 | 108 | var stopSound = function (name) { 109 | var $sound = sounds[name]; 110 | 111 | if (typeof $sound !== "object" || $sound === null) { 112 | return; 113 | } 114 | 115 | $sound.pause(); 116 | try { 117 | $sound.currentTime = 0; 118 | } catch (e) {} 119 | }; 120 | 121 | 122 | var killSound = function (name) { 123 | var $sound = sounds[name]; 124 | 125 | if (typeof $sound !== "object" || $sound === null) { 126 | return; 127 | } 128 | 129 | try { 130 | sounds[name].src = ""; 131 | } catch (e) {} 132 | sounds[name] = null; 133 | }; 134 | 135 | 136 | // Plugin methods 137 | $.ionSound = function (options) { 138 | 139 | settings = $.extend({ 140 | sounds: [ 141 | "water_droplet" 142 | ], 143 | path: "static/sounds/", 144 | multiPlay: true, 145 | volume: "0.5" 146 | }, options); 147 | 148 | soundsNum = settings.sounds.length; 149 | 150 | if (typeof Audio === "function" || typeof Audio === "object") { 151 | for (i = 0; i < soundsNum; i += 1) { 152 | createSound(settings.sounds[i]); 153 | } 154 | } 155 | 156 | $.ionSound.play = function (name) { 157 | playSound(name); 158 | }; 159 | $.ionSound.stop = function (name) { 160 | stopSound(name); 161 | }; 162 | $.ionSound.kill = function (name) { 163 | killSound(name); 164 | }; 165 | }; 166 | 167 | // addSound function added by vkaravir Jan 21st 2014 168 | $.ionSound.addSound = function(soundConf) { 169 | // only add the sound once 170 | if (sounds[soundConf]) { return; } 171 | settings.sounds.push(soundConf); 172 | createSound(soundConf); 173 | }; 174 | 175 | 176 | $.ionSound.destroy = function () { 177 | for (i = 0; i < soundsNum; i += 1) { 178 | sounds[settings.sounds[i]] = null; 179 | } 180 | soundsNum = 0; 181 | $.ionSound.play = function () {}; 182 | $.ionSound.stop = function () {}; 183 | $.ionSound.kill = function () {}; 184 | }; 185 | 186 | }(jQuery)); -------------------------------------------------------------------------------- /test/utils/qunit.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * QUnit 1.14.0 3 | * http://qunitjs.com/ 4 | * 5 | * Copyright 2013 jQuery Foundation and other contributors 6 | * Released under the MIT license 7 | * http://jquery.org/license 8 | * 9 | * Date: 2014-01-31T16:40Z 10 | */ 11 | 12 | /** Font Family and Sizes */ 13 | 14 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult { 15 | font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif; 16 | } 17 | 18 | #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; } 19 | #qunit-tests { font-size: smaller; } 20 | 21 | 22 | /** Resets */ 23 | 24 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter { 25 | margin: 0; 26 | padding: 0; 27 | } 28 | 29 | 30 | /** Header */ 31 | 32 | #qunit-header { 33 | padding: 0.5em 0 0.5em 1em; 34 | 35 | color: #8699A4; 36 | background-color: #0D3349; 37 | 38 | font-size: 1.5em; 39 | line-height: 1em; 40 | font-weight: 400; 41 | 42 | border-radius: 5px 5px 0 0; 43 | } 44 | 45 | #qunit-header a { 46 | text-decoration: none; 47 | color: #C2CCD1; 48 | } 49 | 50 | #qunit-header a:hover, 51 | #qunit-header a:focus { 52 | color: #FFF; 53 | } 54 | 55 | #qunit-testrunner-toolbar label { 56 | display: inline-block; 57 | padding: 0 0.5em 0 0.1em; 58 | } 59 | 60 | #qunit-banner { 61 | height: 5px; 62 | } 63 | 64 | #qunit-testrunner-toolbar { 65 | padding: 0.5em 0 0.5em 2em; 66 | color: #5E740B; 67 | background-color: #EEE; 68 | overflow: hidden; 69 | } 70 | 71 | #qunit-userAgent { 72 | padding: 0.5em 0 0.5em 2.5em; 73 | background-color: #2B81AF; 74 | color: #FFF; 75 | text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; 76 | } 77 | 78 | #qunit-modulefilter-container { 79 | float: right; 80 | } 81 | 82 | /** Tests: Pass/Fail */ 83 | 84 | #qunit-tests { 85 | list-style-position: inside; 86 | } 87 | 88 | #qunit-tests li { 89 | padding: 0.4em 0.5em 0.4em 2.5em; 90 | border-bottom: 1px solid #FFF; 91 | list-style-position: inside; 92 | } 93 | 94 | #qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running { 95 | display: none; 96 | } 97 | 98 | #qunit-tests li strong { 99 | cursor: pointer; 100 | } 101 | 102 | #qunit-tests li a { 103 | padding: 0.5em; 104 | color: #C2CCD1; 105 | text-decoration: none; 106 | } 107 | #qunit-tests li a:hover, 108 | #qunit-tests li a:focus { 109 | color: #000; 110 | } 111 | 112 | #qunit-tests li .runtime { 113 | float: right; 114 | font-size: smaller; 115 | } 116 | 117 | .qunit-assert-list { 118 | margin-top: 0.5em; 119 | padding: 0.5em; 120 | 121 | background-color: #FFF; 122 | 123 | border-radius: 5px; 124 | } 125 | 126 | .qunit-collapsed { 127 | display: none; 128 | } 129 | 130 | #qunit-tests table { 131 | border-collapse: collapse; 132 | margin-top: 0.2em; 133 | } 134 | 135 | #qunit-tests th { 136 | text-align: right; 137 | vertical-align: top; 138 | padding: 0 0.5em 0 0; 139 | } 140 | 141 | #qunit-tests td { 142 | vertical-align: top; 143 | } 144 | 145 | #qunit-tests pre { 146 | margin: 0; 147 | white-space: pre-wrap; 148 | word-wrap: break-word; 149 | } 150 | 151 | #qunit-tests del { 152 | background-color: #E0F2BE; 153 | color: #374E0C; 154 | text-decoration: none; 155 | } 156 | 157 | #qunit-tests ins { 158 | background-color: #FFCACA; 159 | color: #500; 160 | text-decoration: none; 161 | } 162 | 163 | /*** Test Counts */ 164 | 165 | #qunit-tests b.counts { color: #000; } 166 | #qunit-tests b.passed { color: #5E740B; } 167 | #qunit-tests b.failed { color: #710909; } 168 | 169 | #qunit-tests li li { 170 | padding: 5px; 171 | background-color: #FFF; 172 | border-bottom: none; 173 | list-style-position: inside; 174 | } 175 | 176 | /*** Passing Styles */ 177 | 178 | #qunit-tests li li.pass { 179 | color: #3C510C; 180 | background-color: #FFF; 181 | border-left: 10px solid #C6E746; 182 | } 183 | 184 | #qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; } 185 | #qunit-tests .pass .test-name { color: #366097; } 186 | 187 | #qunit-tests .pass .test-actual, 188 | #qunit-tests .pass .test-expected { color: #999; } 189 | 190 | #qunit-banner.qunit-pass { background-color: #C6E746; } 191 | 192 | /*** Failing Styles */ 193 | 194 | #qunit-tests li li.fail { 195 | color: #710909; 196 | background-color: #FFF; 197 | border-left: 10px solid #EE5757; 198 | white-space: pre; 199 | } 200 | 201 | #qunit-tests > li:last-child { 202 | border-radius: 0 0 5px 5px; 203 | } 204 | 205 | #qunit-tests .fail { color: #000; background-color: #EE5757; } 206 | #qunit-tests .fail .test-name, 207 | #qunit-tests .fail .module-name { color: #000; } 208 | 209 | #qunit-tests .fail .test-actual { color: #EE5757; } 210 | #qunit-tests .fail .test-expected { color: #008000; } 211 | 212 | #qunit-banner.qunit-fail { background-color: #EE5757; } 213 | 214 | 215 | /** Result */ 216 | 217 | #qunit-testresult { 218 | padding: 0.5em 0.5em 0.5em 2.5em; 219 | 220 | color: #2B81AF; 221 | background-color: #D2E0E6; 222 | 223 | border-bottom: 1px solid #FFF; 224 | } 225 | #qunit-testresult .module-name { 226 | font-weight: 700; 227 | } 228 | 229 | /** Fixture */ 230 | 231 | #qunit-fixture { 232 | position: absolute; 233 | top: -10000px; 234 | left: -10000px; 235 | width: 1000px; 236 | height: 1000px; 237 | } 238 | -------------------------------------------------------------------------------- /extras/stack.js: -------------------------------------------------------------------------------- 1 | (function ($) { 2 | "use strict"; 3 | if (typeof JSAV === "undefined") { return; } 4 | 5 | var Stack = function (jsav, values, options) { 6 | this.jsav = jsav; 7 | if (!$.isArray(values)) { 8 | options = values; 9 | values = []; 10 | } 11 | this.options = $.extend({visible: true, xtransition: 10, ytransition: -5, autoresize: true}, options); 12 | var el = this.options.element || $("
    "); 13 | el.addClass("jsavstack"); 14 | for (var key in this.options) { 15 | var val = this.options[key]; 16 | if (this.options.hasOwnProperty(key) && typeof(val) === "string" || typeof(val) === "number" || typeof(val) === "boolean") { 17 | el.attr("data-" + key, val); 18 | } 19 | } 20 | if (!this.options.element) { 21 | $(jsav.canvas).append(el); 22 | } 23 | this.element = el; 24 | this.element.attr({"id": this.id()}); 25 | if (this.options.autoresize) { 26 | el.addClass("jsavautoresize"); 27 | } 28 | for (var i = values.length; i--; ) { 29 | this.addFirst(values[i]); 30 | } 31 | if (values.length > 0) { 32 | this.layout(); 33 | } 34 | JSAV.utils._helpers.handlePosition(this); 35 | JSAV.utils._helpers.handleVisibility(this, this.options); 36 | }; 37 | JSAV.utils.extend(Stack, JSAV._types.ds.List); 38 | var stackproto = Stack.prototype; 39 | 40 | stackproto.newNode = function (value, options) { 41 | return new StackNode(this, value, $.extend({first: false}, this.options, options)); 42 | }; 43 | 44 | stackproto.layout = function (options) { 45 | var layoutAlg = $.extend({}, this.options, options).layout || "_default"; 46 | return this.jsav.ds.layout.stack[layoutAlg](this, options); 47 | }; 48 | 49 | var StackNode = function (container, value, options) { 50 | this.jsav = container.jsav; 51 | this.container = container; 52 | this._next = options.next; 53 | this._value = value; 54 | this.options = $.extend(true, {visible: true}, options); 55 | var el = $("
    " + this._valstring(value) + "
    "), 56 | valtype = typeof(value); 57 | if (valtype === "object") { valtype = "string"; } 58 | this.element = el; 59 | el.addClass("jsavnode jsavstacknode") 60 | .attr({"data-value": value, "id": this.id(), "data-value-type": valtype }) 61 | .data("node", this); 62 | if ("first" in options && options.first) { 63 | this.container.element.prepend(el); 64 | } else { 65 | this.container.element.append(el); 66 | } 67 | JSAV.utils._helpers.handleVisibility(this, this.options); 68 | }; 69 | JSAV.utils.extend(StackNode, JSAV._types.ds.ListNode); 70 | var stacknodeproto = StackNode.prototype; 71 | 72 | 73 | stacknodeproto._setnext = JSAV.anim(function (newNext, options) { 74 | var oldNext = this._next; 75 | this._next = newNext; 76 | return [oldNext]; 77 | }); 78 | 79 | stacknodeproto.zIndex = JSAV.anim(function (index) { 80 | var oldVal = ~~this.element.css("z-index"), 81 | node = this; 82 | if (this.jsav._shouldAnimate()) { // only animate when playing, not when recording 83 | $({ z: oldVal }).animate({ z: index }, { 84 | step: function() { 85 | node.element.css('zIndex', ~~this.z); 86 | }, 87 | duration: this.jsav.SPEED 88 | }); 89 | } else { 90 | this.element.css("z-index", index); 91 | } 92 | return [oldVal]; 93 | }); 94 | 95 | var stackLayout = function (stack, options) { 96 | var curNode = stack.first(), 97 | prevNode, 98 | pos = {}, 99 | xTrans = stack.options.xtransition, 100 | yTrans = stack.options.ytransition, 101 | opts = $.extend({}, stack.options, options), 102 | width = 0, 103 | height = 0, 104 | left = 0, 105 | posData = [], 106 | zPos; 107 | 108 | while (curNode) { 109 | if (prevNode) { 110 | pos = { 111 | left: pos.left + xTrans, 112 | top: pos.top + yTrans 113 | }; 114 | zPos -= 1; 115 | width += Math.abs(xTrans); 116 | if ((yTrans < 0 && pos.top < posData[0].nodePos.top) || (yTrans > 0 && pos.top > posData[0].nodePos.top)) { 117 | height += Math.abs(yTrans); 118 | } 119 | if (stack.options.ytransition < 0) { 120 | yTrans += 1; 121 | } 122 | } else { 123 | pos = { 124 | left: (xTrans < 0? stack.size(): 0) * -xTrans, 125 | top: (yTrans < 0? Math.min(stack.size(), -yTrans): 0) * -(yTrans - 1)/2 126 | }; 127 | zPos = stack.size(); 128 | width = curNode.element.outerWidth(); 129 | height = curNode.element.outerHeight(); 130 | } 131 | posData.push({node: curNode, nodePos: pos, zPos: zPos}); 132 | 133 | prevNode = curNode; 134 | curNode = curNode.next(); 135 | } 136 | if (stack.size() === 0) { 137 | var tmpNode = stack.newNode(""); 138 | width = tmpNode.element.outerWidth(); 139 | height = tmpNode.element.outerHeight(); 140 | tmpNode.clear(); 141 | } 142 | 143 | if (stack.options.center) { 144 | left = ($(stack.jsav.canvas).width() - width) / 2; 145 | } else { 146 | left = stack.element.position().left; 147 | } 148 | 149 | if (!opts.boundsOnly) { 150 | // ..update stack size and position.. 151 | stack.css({width: width, height: height, left: left}); 152 | // .. and finally update the node positions 153 | // doing the size first makes the animation look smoother by reducing some flicker 154 | for (var i = 0; i < posData.length; i++) { 155 | var posItem = posData[i]; 156 | posItem.node.zIndex(posItem.zPos); 157 | posItem.node.css(posItem.nodePos); 158 | } 159 | } 160 | return { width: width, height: height, left: left, top: stack.element.position().top }; 161 | }; 162 | 163 | JSAV.ext.ds.layout.stack = { 164 | "_default": stackLayout 165 | }; 166 | 167 | JSAV.ext.ds.stack = function(values, options) { 168 | return new Stack(this, values, options); 169 | }; 170 | 171 | }(jQuery)); -------------------------------------------------------------------------------- /examples/pointers.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Linked Pointer Example 5 | 6 | 25 | 26 | 27 |

    JSAV slideshow for pointer

    28 |
    29 |
    30 |

    31 |
    32 |
    33 | 34 | 35 | 36 | 37 | 38 | 153 | 154 | 155 | -------------------------------------------------------------------------------- /src/translations.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module that contains the translation implementation. 3 | * Depends on core.js, utils.js 4 | */ 5 | /*global JSAV, jQuery */ 6 | (function($) { 7 | "use strict"; 8 | if (typeof JSAV === "undefined") { return; } 9 | 10 | JSAV._translations = { 11 | "en": { 12 | // animation control button titles 13 | "backwardButtonTitle": "Backward", 14 | "forwardButtonTitle": "Forward", 15 | "beginButtonTitle": "Begin", 16 | "endButtonTitle": "End", 17 | 18 | "resetButtonTitle": "Reset", 19 | "undoButtonTitle": "Undo", 20 | "modelButtonTitle": "Model Answer", 21 | "gradeButtonTitle": "Grade", 22 | 23 | "modelWindowTitle": "Model Answer", 24 | 25 | "feedbackLabel":"Grading Mode: ", 26 | "continuous": "Continuous", 27 | "atend": "At end", 28 | 29 | "fixLabel": "Continuous feedback behaviour", 30 | "undo": "Undo incorrect step", 31 | "fix": "Fix incorrect step", 32 | 33 | "scoreLabel": "Score:", 34 | "remainingLabel": "Points remaining:", 35 | "lostLabel": "Points lost:", 36 | "doneLabel": "DONE", 37 | 38 | "yourScore": "Your score:", 39 | "fixedSteps": "Fixed incorrect steps:", 40 | 41 | "fixedPopup": "Your last step was incorrect. Your work has been replaced with the correct step so that you can continue.", 42 | "fixFailedPopup": "Your last step was incorrect and I should fix your solution, but don't know how. So it was undone and you can try again.", 43 | "undonePopup": "Your last step was incorrect. Things are reset to the beginning of the step so that you can try again.", 44 | 45 | "settings": "Settings", 46 | "animationSpeed" : "Animation speed", 47 | "(slowFast)": "(slow - fast)", 48 | "valueBetween1And10": "Value between 1 (Slow) and 10 (Fast).", 49 | "save": "Save", 50 | "saved": "Saved...", 51 | 52 | "questionClose": "Close", 53 | "questionCheck": "Check Answer", 54 | "questionCorrect": "Correct!", 55 | "questionIncorrect": "Incorrect" 56 | }, 57 | "fi": { 58 | "backwardButtonTitle": "Askel taaksepäin", 59 | "forwardButtonTitle": "Askel eteenpäin", 60 | "beginButtonTitle": "Kelaa alkuun", 61 | "endButtonTitle": "Kelaa loppuun", 62 | 63 | "resetButtonTitle": "Uudelleen", 64 | "undoButtonTitle": "Kumoa", 65 | "modelButtonTitle": "Mallivastaus", 66 | "gradeButtonTitle": "Arvostele", 67 | 68 | "modelWindowTitle": "Mallivastaus", 69 | 70 | "feedbackLabel":"Arvostelumuoto: ", 71 | "continuous": "Jatkuva", 72 | "atend": "Lopussa", 73 | 74 | "fixLabel": "Jatkuvan arvostelun asetukset", 75 | "undo": "Kumoa väärin menneet askeleet", 76 | "fix": "Korjaa väärin menneet askeleet", 77 | 78 | "scoreLabel": "Pisteet:", 79 | "remainingLabel": "Pisteitä jäljellä:", 80 | "lostLabel": "Menetetyt pisteet:", 81 | "doneLabel": "VALMIS", 82 | 83 | "yourScore": "Sinun pisteesi:", 84 | "fixedSteps": "Korjatut askeleet:", 85 | 86 | "fixedPopup": "Viime askeleesi meni väärin. Se on korjattu puolestasi, niin että voit jatkaa tehtävää.", 87 | "fixFailedPopup": "Viime askeleesi meni väärin ja minun tulisi korjata se. En kuitenkaan osaa korjata sitä, joten olen vain kumonnut sen.", 88 | "undonePopup": "Viime askeleesi meni väärin. Askel on kumottu, niin että voit yrittää uudelleen.", 89 | 90 | "settings": "Asetukset", 91 | "animationSpeed" : "Animointinopeus", 92 | "(slowFast)": "(hidas - nopea)", 93 | "valueBetween1And10": "Anna arvo 1:n (hidas) ja 10:n (nopea) välillä.", 94 | "save": "Tallenna", 95 | "saved": "Tallennettu...", 96 | 97 | "questionClose": "Sulje", 98 | "questionCheck": "Tarkista vastaus", 99 | "questionCorrect": "Oikein!", 100 | "questionIncorrect": "Väärin" 101 | 102 | }, 103 | "sv": { 104 | "resetButtonTitle": "Börja om", 105 | "undoButtonTitle": "Ångra", 106 | "modelButtonTitle": "Visa lösning", 107 | "gradeButtonTitle": "Poängsätt", 108 | 109 | "modelWindowTitle": "Lösning", 110 | 111 | "feedbackLabel":"Rättningsalternativ: ", 112 | "continuous": "Rätta kontinuerligt", 113 | "atend": "Poängsätt i slutet", 114 | 115 | "fixLabel": "Alternativ för kontinuerlig rättning", 116 | "undo": "Ångra felaktigt steg", 117 | "fix": "Åtgärda felaktigt steg", 118 | 119 | "scoreLabel": "Poäng:", 120 | "remainingLabel": "Kvarvarande poäng:", 121 | "lostLabel": "Förlorade poäng:", 122 | "doneLabel": "FÄRDIG!", 123 | 124 | "yourScore": "Din poäng:", 125 | "fixedSteps": "Åtgärdade felaktiga steg:", 126 | 127 | "fixedPopup": "Ditt senaste steg var felaktigt. Det har ersatts med det korrekta steget så att du kan fortsätta.", 128 | "fixFailedPopup": "Ditt senaste steg var felaktigt men jag vet inte hur det bör åtgärdas. Därför har ditt steg ångrats och du kan försöka igen.", 129 | "undonePopup": "Ditt senaste steg var felaktigt. Steget har ångrats så att du kan försöka igen.", 130 | 131 | "settings": "Inställningar", 132 | "animationSpeed" : "Animationshastighet", 133 | "(slowFast)": "(långsamt - snabbt)", 134 | "valueBetween1And10": "Värde mellan 1 (långsamt) och 10 (snabbt).", 135 | "save": "Spara", 136 | "saved": "Sparat..." 137 | }, 138 | "fr": { 139 | "resetButtonTitle": "Reinitialiser", 140 | "undoButtonTitle": "Annuler", 141 | "modelButtonTitle": "Solution", 142 | "gradeButtonTitle": "Score", 143 | 144 | "modelWindowTitle": "Solution", 145 | 146 | "feedbackLabel":"Type de controle: ", 147 | "continuous": "Continue", 148 | "atend": "A la fin", 149 | 150 | "fixLabel": "Parametres de controle continue", 151 | "undo": "Annuler les étapes incorrectes", 152 | "fix": "Corriger les étapes incorrectes", 153 | 154 | "scoreLabel": "Score:", 155 | "remainingLabel": "Points restants:", 156 | "lostLabel": "Points perdus:", 157 | "doneLabel": "Activite terminée", 158 | 159 | "yourScore": "Votre score:", 160 | "fixedSteps": "Corriger les étapes incorrectes:", 161 | 162 | "fixedPopup": "Votre dernière action etait incorrecte. Votre action a été corrigée pour vous permettre de poursuivre.", 163 | "fixFailedPopup": "Votre dernière action etait incorrecte et je ne sais pas comment la corriger. Votre action a ete annulée, essayez a nouveau.", 164 | "undonePopup": "Votre dernière action etait incorrecte. La simulation a été reinitialisée. Essayez a nouveau.", 165 | 166 | "settings": "Paramètres", 167 | "animationSpeed" : "Vitesse", 168 | "(slowFast)": "(lent - rapide)", 169 | "valueBetween1And10": "Vitesse entre 1 (Lent) et 10 (Rapide).", 170 | "save": "Sauvegarder", 171 | "saved": "Sauvegarde complète..." 172 | } 173 | }; 174 | 175 | JSAV.init(function (options) { 176 | var language = options.lang || "en"; 177 | if (typeof JSAV._translations[language] === "object") { 178 | this._translate = JSAV.utils.getInterpreter(JSAV._translations, language); 179 | } else { 180 | this._translate = JSAV.utils.getInterpreter(JSAV._translations, "en"); 181 | } 182 | }); 183 | 184 | }(jQuery)); 185 | -------------------------------------------------------------------------------- /src/core.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module that contains JSAV core. 3 | */ 4 | /*global JSAV, jQuery, Raphael */ 5 | (function($) { 6 | "use strict"; 7 | var JSAV = function() { 8 | create.apply(this, arguments); 9 | }; 10 | JSAV.position = function(elem) { 11 | var $el = $(elem), 12 | offset = $el.position(), 13 | translate = null;//$el.css("transform").translate; // requires jquery.transform.light.js!! 14 | if (translate) { 15 | return {left: offset.left + translate[0], top: offset.top + translate[1]}; 16 | } else { return offset; } 17 | }; 18 | var jsavproto = JSAV.prototype; 19 | jsavproto.getSvg = function() { 20 | if (!this.svg) { // lazily create the SVG overlay only when needed 21 | this.svg = Raphael(this.canvas[0]); 22 | // this.svg.renderfix(); 23 | var style = this.svg.canvas.style; 24 | style.position = "absolute"; 25 | } 26 | return this.svg; 27 | }; 28 | jsavproto.id = function() { 29 | var id = this.container[0].id; 30 | if (!id) { 31 | id = JSAV.utils.createUUID(); 32 | this.container[0].id = id; 33 | } 34 | return id; 35 | }; 36 | jsavproto.clear = function() { 37 | // clear the container and find the new ref to canvas 38 | this.container.html(this._initialHTML); 39 | this.canvas = this.container.find(".jsavcanvas"); 40 | }; 41 | JSAV._types = {}; // for exposing types of JSAV for customization 42 | JSAV.ext = {}; // for extensions 43 | JSAV.init = function(f) { // for initialization functions 44 | JSAV.init.functions.push(f); 45 | }; 46 | JSAV.init.functions = []; 47 | 48 | var create = function() { 49 | // this will point to a newly-created JSAV instance 50 | if (typeof arguments[0] === "string") { 51 | this.container = $(document.getElementById(arguments[0])); 52 | } else if (arguments[0] instanceof HTMLElement) { 53 | this.container = $(arguments[0]); // make sure it is jQuery object 54 | } else if (arguments[0] && typeof arguments[0] === "object" && arguments[0].constructor === jQuery) { 55 | this.container = arguments[0]; 56 | } 57 | 58 | var defaultOptions = $.extend({ 59 | autoresize: true, 60 | scroll: true 61 | }, window.JSAV_OPTIONS); 62 | // if the container was set based on the first argument, options are the second arg 63 | if (this.container) { 64 | this.options = $.extend(true, defaultOptions, arguments[1]); 65 | } else { // otherwise assume the first argument is options (if exists) 66 | this.options = $.extend(true, defaultOptions, arguments[0]); 67 | // set the element option as the container 68 | this.container = $(this.options.element); 69 | } 70 | 71 | // initialHTML will be logged as jsav-init, this._initialHTML used in clear 72 | var initialHTML = this.container.clone().wrap("

    ").parent().html(); 73 | this._initialHTML = this.container.html(); 74 | 75 | this.container.addClass("jsavcontainer"); 76 | this.canvas = this.container.find(".jsavcanvas"); 77 | if (this.canvas.size() === 0) { 78 | this.canvas = $("

    ").addClass("jsavcanvas").appendTo(this.container); 79 | } 80 | // element used to block events when animating 81 | var shutter = $("
    ").appendTo(this.container); 82 | this._shutter = shutter; 83 | 84 | this.RECORD = true; 85 | jQuery.fx.off = true; // by default we are recording changes, not animating them 86 | // initialize stuff from init namespace 87 | initializations(this, this.options); 88 | // add all plugins from ext namespace 89 | extensions(this, this, JSAV.ext); 90 | 91 | this.logEvent({ type: "jsav-init", initialHTML: initialHTML }); 92 | }; 93 | function initializations(jsav, options) { 94 | var fs = JSAV.init.functions; 95 | for (var i = 0; i < fs.length; i++) { 96 | if ($.isFunction(fs[i])) { 97 | fs[i].call(jsav, options); 98 | } 99 | } 100 | } 101 | function extensions(jsav, con, add) { 102 | for (var prop in add) { 103 | if (add.hasOwnProperty(prop) && !(prop in con)) { 104 | switch (typeof add[prop]) { 105 | case "function": 106 | (function (f) { 107 | con[prop] = con === jsav ? f : function () { return f.apply(jsav, arguments); }; 108 | }(add[prop])); 109 | break; 110 | case "object": 111 | con[prop] = con[prop] || {}; 112 | extensions(jsav, con[prop], add[prop]); 113 | break; 114 | default: 115 | con[prop] = add[prop]; 116 | break; 117 | } 118 | } 119 | } 120 | } 121 | 122 | // register a handler for autoresizing the jsavcanvas 123 | JSAV.init(function() { 124 | // in a JSAV init function, this will be the just-created JSAV instance 125 | if (this.options.autoresize) { 126 | var that = this; 127 | // register event handler for jsav-updaterelative which is triggered on each step 128 | this.container.on("jsav-updaterelative", function() { 129 | // collect max top and left positions of all JSAV objects 130 | var maxTop = parseInt(that.canvas.css("minHeight"), 10), 131 | maxLeft = parseInt(that.canvas.css("minWidth"), 10); 132 | 133 | // go through all elements inside jsavcanvas 134 | that.canvas.children().each(function(index, item) { 135 | var $item = $(item), 136 | itemPos = $item.position(); 137 | // ignore SVG, since it will be handled differently since it's sized 100%x100% 138 | if (item.nodeName.toLowerCase() !== "svg") { 139 | maxTop = Math.max(maxTop, itemPos.top + $item.outerHeight(true)); 140 | maxLeft = Math.max(maxLeft, itemPos.left + $item.outerWidth(true)); 141 | } 142 | }); 143 | if (that.svg) { // handling of SVG 144 | var curr = that.svg.bottom, // start from the element in the behind 145 | bbox, strokeWidth; 146 | while (curr) { // iterate all SVG objects in Raphael 147 | bbox = curr.getBBox(); 148 | strokeWidth = curr.attr("stroke-width"); 149 | maxTop = Math.max(maxTop, bbox.y2 + strokeWidth); 150 | maxLeft = Math.max(maxLeft, bbox.x2 + strokeWidth); 151 | curr = curr.next; 152 | } 153 | } 154 | // limit minWidth to parent width if scroll is set to true 155 | if (that.options.scroll) { 156 | var parentWidth = that.canvas.parent().width(); 157 | maxLeft = Math.min(maxLeft, parentWidth); 158 | } 159 | // set minheight and minwidth on the jsavcanvas element 160 | that.canvas.css({"minHeight": maxTop, "minWidth": maxLeft}); 161 | }); 162 | } 163 | }); // end autoresize handler 164 | 165 | if (window) { 166 | window.JSAV = JSAV; 167 | 168 | // Set narration options here so that developers can access and modify the 169 | // default narration replacement patterns used. 170 | window.JSAV_OPTIONS = { 171 | narration: { 172 | enabled: false, 173 | // specifies replacement patterns for text that should 174 | // not be read by the narrator 175 | replacements: [ 176 | {searchValue: /<[^>]*>/g, replaceValue: ""}, 177 | {searchValue: /\$/g, replaceValue: ""}, 178 | {searchValue: /\\mathbf/gi, replaceValue: ""}, 179 | {searchValue: /\\displaystyle/gi, replaceValue: ""}, 180 | {searchValue: /\\mbox/gi, replaceValue: ""}, 181 | {searchValue: /n-/gi, replaceValue: "n minus"}, 182 | {searchValue: /m-/gi, replaceValue: "m minus"}, 183 | {searchValue: /%/gi, replaceValue: "remainder"} 184 | ], 185 | // The speechSynthesis API uses IETF language tags. 186 | // For languages that have regional variations, this mapping 187 | // specifies which variation to use for the narration voice. 188 | langMap: { 189 | "en": "en-US", 190 | "fr": "fr-FR" 191 | } 192 | } 193 | }; 194 | } 195 | }(jQuery)); 196 | -------------------------------------------------------------------------------- /src/messages.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module that contains the message output implementations. 3 | * Depends on core.js, anim.js 4 | */ 5 | /*global JSAV, jQuery */ 6 | (function($) { 7 | "use strict"; 8 | if (typeof JSAV === "undefined") { return; } 9 | 10 | var MessageHandler = function(jsav, output) { 11 | this.jsav = jsav; 12 | this.output = output; 13 | if (this.output && "title" in jsav.options) { 14 | this.output.html("
    " + jsav.options.title + "
    "); 15 | } 16 | }; 17 | MessageHandler.prototype.umsg = JSAV.anim(function(msg, options) { 18 | if (options && options.fill && typeof options.fill === "object") { 19 | // replace the labels in the string if the replacements are given in the options 20 | msg = JSAV.utils.replaceLabels(msg, options.fill); 21 | } 22 | var opts = $.extend({color: "black", preserve: false}, options); 23 | if (this.output) { 24 | if (this.output.hasClass("jsavline") && opts.preserve) { 25 | var el = this.output.find("div:last"), 26 | newmsg = "" + msg + ""; 27 | if (el.size() > 0) { // existing content in message output 28 | el.append(newmsg); 29 | } else { // first message 30 | this.output.html("
    " + msg + "
    "); 31 | } 32 | } else if (this.output.hasClass("jsavline")) { 33 | this.output.html("
    " + msg + "
    "); 34 | //} else if (this.output.hasClass("jsavscroll")) { 35 | } else { // e.g. "jsavscroll", which is default 36 | this.output.append("
    " + msg + "
    "); 37 | if (this.output[0]) { 38 | this.output[0].scrollTop = this.output[0].scrollHeight; 39 | } 40 | } 41 | } 42 | // Narrating every call to umsg 43 | if($(this.jsav.container).attr("voice") === "true") { 44 | this.jsav.textToSpeech(msg, this.jsav.options.narration); 45 | } 46 | if (!this.jsav.RECORD) { // trigger events only if not recording 47 | this.jsav.container.trigger("jsav-message", [msg, options]); 48 | } 49 | return this; 50 | }); 51 | MessageHandler.prototype.clear = JSAV.anim(function() { 52 | if (this.output) { 53 | this.output.html(""); 54 | } 55 | }); 56 | MessageHandler.prototype.state = function(newValue) { 57 | if (newValue) { 58 | this.output.html(newValue); 59 | this.jsav.container.trigger("jsav-message", [newValue, this.options]); 60 | // Narrating for Backward buttons 61 | if($(this.jsav.container).attr("voice") === "true") { 62 | this.jsav.textToSpeech(newValue, this.jsav.options.narration); 63 | } 64 | } else { 65 | return this.output.html() || ""; 66 | } 67 | }; 68 | 69 | JSAV.ext.umsg = function(msg, options) { 70 | this._msg.umsg(msg, options); 71 | }; 72 | JSAV.ext.clearumsg = function(msg, options) { 73 | this._msg.clear(); 74 | }; 75 | 76 | JSAV.ext.textToSpeech = function(speechText, options) { 77 | var synth = window.speechSynthesis; 78 | if (typeof synth === "undefined") { 79 | return; 80 | } 81 | var modifiedMsg = speechText; 82 | var replacements = options.overrideReplacements || options.replacements || []; 83 | if (options.appendReplacements) { 84 | replacements = replacements.concat(options.appendReplacements); 85 | } 86 | for (var i = 0; i < replacements.length; i++) { 87 | var replacement = replacements[i]; 88 | modifiedMsg = modifiedMsg.replace(replacement.searchValue, replacement.replaceValue); 89 | } 90 | var u = new SpeechSynthesisUtterance(); 91 | var amISpeaking = synth.speaking; 92 | u.lang = options.lang || 'en'; 93 | u.rate = 1.0; 94 | // Assign the umsg text to the narration file 95 | u.text = modifiedMsg; 96 | if(amISpeaking === true) { 97 | synth.cancel(); 98 | } 99 | synth.speak(u); 100 | }; 101 | 102 | var soundSettings = function(jsav) { 103 | // creating the button element 104 | var $elem = $(""); 105 | // Sound button click event 106 | $elem.click(function() { 107 | for (var j = 0; j < window.length; j++) { 108 | $(".sound", window.frames[j].document).not(this).each(function() { 109 | $(this.offsetParent).parent().find(".jsavcontainer").attr("voice", "false"); 110 | $(this).removeClass("sound").addClass("soundOff"); 111 | }); 112 | } 113 | // SwitchingOff sound buttons not in frames 114 | $(".sound", window.document).not(this).each(function() { 115 | $(this.offsetParent).parent().find(".jsavcontainer").attr("voice", "false"); 116 | $(this).removeClass("sounds").addClass("soundOff"); 117 | }); 118 | //Toggle voice attribute and stop the current voice 119 | var synth = window.speechSynthesis; 120 | var amISpeaking = synth.speaking; 121 | // "inlineav" slideshows 122 | if($(this.offsetParent).attr("voice") !== undefined) { 123 | if($(this.offsetParent).attr("voice") === "true") { 124 | $(this.offsetParent).attr("voice", "false"); 125 | if(amISpeaking === true) { 126 | synth.cancel(); 127 | } 128 | } else { 129 | $(this.offsetParent).attr("voice", "true"); 130 | } //slideshows in a frame 131 | } else if($(this.offsetParent).parent().find(".jsavcontainer").attr("voice") === "true") { 132 | $(this.offsetParent).parent().find(".jsavcontainer").attr("voice", "false"); 133 | if(amISpeaking === true) { 134 | synth.cancel(); 135 | } 136 | } else { 137 | $(this.offsetParent).parent().find(".jsavcontainer").attr("voice", "true"); 138 | } 139 | //Toggle button class sound/soundOff 140 | if($(this).hasClass("sound")) { 141 | $(this).removeClass("sound").addClass("soundOff"); 142 | jsav.logEvent({ 143 | "type": "jsav-narration-off", 144 | "currentStep": jsav.currentStep(), 145 | "totalSteps": jsav.totalSteps() 146 | }); 147 | } else { 148 | var txt = $(this.offsetParent).find(".jsavoutput").clone().find(".MathJax").remove().end().text(); 149 | jsav.textToSpeech(txt, jsav.options.narration); 150 | $(this).removeClass("soundOff").addClass("sound"); 151 | jsav.logEvent({ 152 | "type": "jsav-narration-on", 153 | "currentStep": jsav.currentStep(), 154 | "totalSteps": jsav.totalSteps() 155 | }); 156 | } 157 | }); 158 | return $elem; 159 | }; 160 | 161 | JSAV.init(function(options) { 162 | var output = options.output ? $(options.output) : $(this.container).find(".jsavoutput"); 163 | this._msg = new MessageHandler(this, output); 164 | 165 | var supportsSpeech = typeof window.speechSynthesis !== "undefined"; 166 | if (supportsSpeech && options.narration && options.narration.enabled) { 167 | // determining language code the browser will use to choose a voice 168 | var langMap = this.options.narration.langMap || {}; 169 | var lang = this.options.lang || 'en'; 170 | options.narration.lang = langMap[lang] || lang; 171 | 172 | // adding sound button if there is no sound button in the container and 173 | // the container has both controls and settings button 174 | if(($(this.container).parent().find(".new").length === 0) && 175 | ($(this.container).find(".jsavcontrols").length !== 0) && 176 | ($(this.container).parent().find(".jsavsettings").length !== 0)) { 177 | 178 | $(this.container).parent().find(".jsavsettings").wrap(""); 179 | $(this.container).parent().find(".new").append(soundSettings(this)); 180 | } 181 | //adding voice attribute to the container 182 | $(this.container).attr("voice", "false"); 183 | } 184 | }); 185 | $(window).unload(function() { 186 | var synth = window.speechSynthesis; 187 | synth.cancel(); 188 | }); 189 | }(jQuery)); 190 | -------------------------------------------------------------------------------- /src/matrix.js: -------------------------------------------------------------------------------- 1 | /*global JSAV, jQuery, console */ 2 | (function($) { 3 | "use strict"; 4 | if (typeof JSAV === "undefined") { return; } 5 | 6 | var getIndices = JSAV.utils._helpers.getIndices; 7 | var defaultOptions = {style: "table", 8 | autoresize: true, 9 | center: true, 10 | visible: true}; 11 | /* Matrix data structure for JSAV library. */ 12 | var Matrix = function(jsav, initialData, options) { 13 | this.jsav = jsav; 14 | var i; 15 | if ($.isArray(initialData)) { // initialData contains an array of data 16 | this.options = $.extend({}, defaultOptions, options); 17 | options = this.options; 18 | } else if (typeof initialData === "object") { // initialData is options 19 | this.options = $.extend({}, defaultOptions, initialData); 20 | options = this.options; // cache the options 21 | // we'll create an initialData based on lines and columns options 22 | var temparr = []; 23 | initialData = []; 24 | temparr.length = options.columns; 25 | for (i = options.rows; i--; ) { initialData.push(temparr); } 26 | } else { 27 | console.error("Invalid arguments for initializing a matrix!"); 28 | } 29 | this.element = options.element?$(options.element):$("
    ") 30 | .appendTo(jsav.canvas); // add to DOM 31 | if ('id' in options) { 32 | this.id(options.id, {record: false}); 33 | } 34 | // add a class for the style of the matrix 35 | this.element.addClass("jsavmatrix" + options.style); 36 | 37 | // create arrays within the matrix element 38 | // set visible to false to prevent the array from animating show 39 | var arrayOpts = $.extend({}, options, {center: false, visible: false}), 40 | arrayElem; 41 | 42 | // make sure we don't pass the matrix's id or positioning to the arrays 43 | delete arrayOpts.id; 44 | delete arrayOpts.left; 45 | delete arrayOpts.top; 46 | delete arrayOpts.relativeTo; 47 | this._arrays = []; 48 | for (i = 0; i < initialData.length; i++) { 49 | // create an element for the array and add to options 50 | arrayElem = $("
      "); 51 | arrayOpts.element = arrayElem; 52 | this.element.append(arrayElem); 53 | // initialize the array 54 | this._arrays[i] = jsav.ds.array(initialData[i], arrayOpts); 55 | // set the array visible, visibility will be handled by the matrix element 56 | arrayElem.css("display", "block"); 57 | } 58 | this.options = $.extend(true, {}, options); 59 | JSAV.utils._helpers.handlePosition(this); 60 | this.layout(); 61 | JSAV.utils._helpers.handleVisibility(this, this.options); 62 | }; 63 | JSAV.utils.extend(Matrix, JSAV._types.ds.JSAVDataStructure); 64 | var matrixproto = Matrix.prototype; 65 | 66 | // swap two elements in the matrix, (row1, col1) with (row2, col2) 67 | matrixproto.swap = function(row1, col1, row2, col2, options) { 68 | this.jsav.effects.swapValues(this._arrays[row1], col1, 69 | this._arrays[row2], col2, 70 | options); 71 | return this; 72 | }; 73 | // set or get the state of this structure to be restored in the future 74 | matrixproto.state = function(newState) { 75 | var _arrays = this._arrays, // cache 76 | i, l; 77 | if (newState) { 78 | for (i = 0, l = _arrays.length; i < l; i++) { 79 | _arrays[i].state(newState[i]); 80 | } 81 | return this; 82 | } else { 83 | var state = []; 84 | for (i = 0, l = _arrays.length; i < l; i++) { 85 | state.push(_arrays[i].state()); 86 | } 87 | return state; 88 | } 89 | }; 90 | // layout all the arrays in this matrix 91 | matrixproto.layout = function(options) { 92 | var dimensions, i, 93 | l = this._arrays.length, 94 | maxWidth = -1; 95 | // if we want to center the structure, add the css class to do that 96 | if (this.options.center) { 97 | this.element.addClass("jsavcenter"); 98 | } 99 | for (i = 0; i < l; i++) { 100 | dimensions = this._arrays[i].layout(options); 101 | maxWidth = Math.max(maxWidth, dimensions.width); 102 | } 103 | this.element.width(maxWidth + "px"); 104 | }; 105 | matrixproto.equals = function(other, options) { 106 | var i, l, 107 | _arrays, _other; 108 | if ($.isArray(other)) { 109 | for (i = other.length; i--; ) { 110 | if (!this._arrays[i].equals(other[i], options)) { 111 | return false; 112 | } 113 | } 114 | } else if (other.constructor === Matrix) { 115 | _arrays = this._arrays; 116 | _other = other._arrays; 117 | // if lengths don't match, they can't be the same 118 | if (_other.length !== _arrays.length) { 119 | return false; 120 | } 121 | // iterate over the arrays and compare (starting from end) 122 | for (i = _other.length; i--; ) { 123 | if (!_arrays[i].equals(_other[i], options)) { 124 | return false; 125 | } 126 | } 127 | } else { 128 | console.error("Unknown type of object for comparing with matrix:", other); 129 | return false; 130 | } 131 | // if we made it this far, the matrices are equal 132 | return true; 133 | }; 134 | 135 | // functions of array that we want to add to the matrix 136 | var arrayFunctions = ["highlight", "unhighlight", "isHighlight", "css", "value", 137 | "addClass", "hasClass", "removeClass", "toggleClass"]; 138 | // will return a wrapper for the arrays function with funcname 139 | // the returned function will assume the row index as the first parameter and will 140 | // pass the rest of the arguments to the array function 141 | var arrayFunctionWrapper = function(funcname) { 142 | return function() { 143 | var arrIndex = arguments[0]; 144 | if (typeof arrIndex !== "number") { return; } 145 | var array = this._arrays[arrIndex]; 146 | return array[funcname].apply(array, [].slice.call(arguments, 1)); 147 | }; 148 | }; 149 | // add functions with all the names in arrayFunctions wrapped in the row extension function 150 | for (var i = arrayFunctions.length; i--; ) { 151 | matrixproto[arrayFunctions[i]] = arrayFunctionWrapper(arrayFunctions[i]); 152 | } 153 | 154 | 155 | // events to register as functions on the matrix 156 | var events = ["click", "dblclick", "mousedown", "mousemove", "mouseup", 157 | "mouseenter", "mouseleave"]; 158 | // returns a function for the passed eventType that binds a passed 159 | // function to that eventType for indices in the arrays 160 | var eventhandler = function(eventType) { 161 | return function(data, handler) { 162 | // store reference to this, needed when executing the handler 163 | var self = this; 164 | // bind a jQuery event handler, limit to .jsavindex 165 | this.element.on(eventType, ".jsavindex", function(e) { 166 | var targetArray = $(this).parent(); 167 | // get the row of the clicked element 168 | var row = self.element.find(".jsavarray").index(targetArray); 169 | var col = targetArray.find(".jsavindex").index(this); 170 | // log the event 171 | self.jsav.logEvent({type: "jsav-matrix-" + eventType, 172 | matrixid: self.id(), 173 | row: row, 174 | column: col}); 175 | if ($.isFunction(data)) { // if no custom data.. 176 | // ..bind this to the matrix and call handler 177 | // with params row and column and the event 178 | data.call(self, row, col, e); 179 | } else if ($.isFunction(handler)) { // if custom data is passed 180 | var params = $.isArray(data)?data.slice(0):[data]; // get a cloned array or data as array 181 | params.unshift(col); // add index to first parameter 182 | params.unshift(row); // add index to first parameter 183 | params.push(e); // jQuery event as the last 184 | handler.apply(self, params); // apply the function 185 | } 186 | }); 187 | return this; 188 | }; 189 | }; 190 | // create the event binding functions and add to array prototype 191 | for (i = events.length; i--; ) { 192 | matrixproto[events[i]] = eventhandler(events[i]); 193 | } 194 | matrixproto.on = function(eventName, data, handler) { 195 | eventhandler(eventName).call(this, data, handler); 196 | return this; 197 | }; 198 | 199 | 200 | JSAV._types.ds.Matrix = Matrix; 201 | JSAV.ext.ds.matrix = function(initialData, options) { 202 | return new Matrix(this, initialData, options); 203 | }; 204 | })(jQuery); -------------------------------------------------------------------------------- /src/settings.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module that contains the configurable settings panel implementation 3 | * Depends on core.js, utils.js, trasnlations.js 4 | */ 5 | /*global JSAV, jQuery */ 6 | (function($) { 7 | "use strict"; 8 | if (typeof JSAV === "undefined") { return; } 9 | var speedChoices = [5000, 3000, 1500, 1000, 500, 400, 300, 200, 100, 50]; 10 | var getSpeedChoice = function(speedVal) { 11 | var curval = speedChoices.length - 1; 12 | while (curval && speedChoices[curval] < speedVal) { 13 | curval--; 14 | } 15 | return curval + 1; 16 | }; 17 | var speedSetting = function(settings) { 18 | return function() { 19 | var translate; 20 | if (settings.jsav) { 21 | translate = settings.jsav._translate; 22 | } else { 23 | var lang = "en"; 24 | if (window.JSAV_OPTIONS && 25 | window.JSAV_OPTIONS.lang && 26 | typeof JSAV._translations[window.JSAV_OPTIONS.lang] === "object") 27 | { 28 | lang = window.JSAV_OPTIONS.lang; 29 | } 30 | translate = JSAV.utils.getInterpreter(JSAV._translations, lang); 31 | } 32 | var curSpeed = JSAV.ext.SPEED; 33 | var rangeSupported = !!$.support.inputTypeRange; 34 | // get the closest speed choice to the current speed 35 | var curval = getSpeedChoice(curSpeed); 36 | // add explanation if using range slider, help text otherwise 37 | var $elem = $('
      ' + translate("animationSpeed") + (rangeSupported?' ' + translate("(slowFast)"):'') + 38 | ': ' + 39 | (rangeSupported?'':'
      ' + translate("valueBetween1And10")) + 40 | '
      '); 41 | // event handler function for storing the speed 42 | var speedChangeHandler = function() { 43 | var speed = parseInt($(this).val(), 10); 44 | if (isNaN(speed) || speed < 1 || speed > 10) { return; } 45 | speed = speedChoices[speed - 1]; // speed in milliseconds 46 | curSpeed = speed; 47 | JSAV.ext.SPEED = speed; 48 | //trigger speed change event to update all AVs on the page 49 | $(document).trigger("jsav-speed-change", speed); 50 | if (typeof localStorage !== 'undefined' && localStorage) { 51 | localStorage.setItem("jsav-speed", speed); 52 | } 53 | }; 54 | // set the value and add a change event listener 55 | var $inputElem = $elem.find("input").val(curval); 56 | if (rangeSupported) { 57 | $inputElem.change(speedChangeHandler); 58 | } else { 59 | $elem.find("button").click(function() { 60 | speedChangeHandler.call($inputElem); 61 | var savedElem = $("" + translate("saved") + ""); 62 | setTimeout(function() { savedElem.fadeOut(); }, 1000); 63 | $(this).after(savedElem); 64 | }); 65 | } 66 | 67 | // return the element 68 | return $elem; 69 | }; 70 | }; 71 | 72 | /* Creates an input component to be used in the settings panel. varname should be unique 73 | within the document. Options can specify the label of the component, in which case 74 | a label element is created. Option value specifies the default value of the element. 75 | Every other option will be set as an attribute of the input element. */ 76 | var createInputComponent = function(varname, options) { 77 | var label, 78 | opts = $.extend({"type": "text"}, options), 79 | input = $(''); 81 | if ('label' in opts) { 82 | label = $('"); 83 | } 84 | if ('value' in opts) { 85 | input.val(opts.value); 86 | } 87 | for (var attr in opts) { 88 | if (['label', 'value', 'type'].indexOf(attr) === -1) { 89 | input.attr(attr, opts[attr]); 90 | } 91 | } 92 | return $('
      ').append(label).append(input); 93 | }; 94 | 95 | /* Creates a select component to be used in the settings panel. varname should be unique 96 | within the document. Options can specify the label of the component, in which case 97 | a label element is created. Option value specifies the default value of the element. 98 | Option options should be a map where every key-value pair will make for an option element 99 | in the form. Every other option will be set as an attribute of the input element. */ 100 | var createSelectComponent = function(varname, options) { 101 | var label, 102 | select = $('' + 17 | '' + 18 | '' + 19 | '' + 20 | '
      '); 21 | if (jsav.options.showQuestions) { 22 | $elem.find("#" + idPrefix + "Yes").prop("checked", true); 23 | } else { 24 | $elem.find("#" + idPrefix + "No").prop("checked", true); 25 | } 26 | $elem.find('input').on("change", function() { 27 | jsav.options.showQuestions = ($(this).val() === "true"); 28 | }); 29 | return $elem; 30 | }; 31 | }; 32 | 33 | var createInputComponent = function(label, itemtype, options) { 34 | var labelElem = $('"), 35 | input = $(''); 37 | $.each(options, function(key, value) { 38 | if (BLOCKED_ATTRIBUTES.indexOf(key) === -1) { 39 | input.attr(key, value); 40 | } 41 | }); 42 | return $('
      ').append(input).append(labelElem); 43 | }; 44 | var feedbackFunction = function($elems) { 45 | var cbs = $elems.find('[type="checkbox"]'), 46 | that = this, 47 | correct = true; 48 | if (cbs.size() === 0) { 49 | cbs = $elems.find('[type="radio"]'); 50 | } 51 | var answers = [], answer; 52 | cbs.each(function(index, item) { 53 | var qi = that.choiceById(item.id); 54 | var $item = $(item); 55 | answer = { 56 | label: qi.label, 57 | selected: !!$item.prop("checked"), 58 | type: $item.attr("type"), 59 | correct: true // assume correct and mark false if incorrect 60 | }; 61 | if (!!$item.prop("checked") !== !!qi.options.correct) { 62 | correct = false; 63 | answer.correct = false; 64 | //return false; // break the loop 65 | } 66 | answers.push(answer); 67 | }); 68 | $elems.filter(".jsavfeedback") 69 | .html(correct?this.jsav._translate('questionCorrect'):this.jsav._translate('questionIncorrect')) 70 | .removeClass("jsavcorrect jsavincorrect") 71 | .addClass(correct?"jsavcorrect":"jsavincorrect"); 72 | $elems.filter('[type="submit"]').remove(); 73 | return {correct: correct, answers: answers}; 74 | // TODO: add support for points, feedback comments etc. 75 | }; 76 | 77 | var qTypes = {}; 78 | qTypes.TF = { // True-False type question 79 | init: function() { 80 | this.name = createUUID(); 81 | this.choices[0] = new QuestionItem(this.options.falseLabel || "False", 82 | "radio", {name: this.name, correct: !this.options.correct}); 83 | this.choices[1] = new QuestionItem(this.options.trueLabel || "True", 84 | "radio", {name: this.name, correct: !!this.options.correct}); 85 | this.correctChoice = function(correctVal) { 86 | if (correctVal) { 87 | this.choices[1].correct = true; 88 | } else { 89 | this.choices[0].correct = true; 90 | } 91 | }; 92 | }, 93 | feedback: feedbackFunction 94 | }; 95 | qTypes.MC = { 96 | init: function() { 97 | this.name = createUUID(); 98 | }, 99 | addChoice: function(label, options) { 100 | this.choices.push(new QuestionItem(label, "radio", $.extend({name: this.name}, options))); 101 | return this; 102 | }, 103 | feedback: feedbackFunction 104 | }; 105 | qTypes.MS = { 106 | addChoice: function(label, options) { 107 | this.choices.push(new QuestionItem(label, "checkbox", $.extend({}, options))); 108 | return this; 109 | }, 110 | feedback: feedbackFunction 111 | }; 112 | 113 | var QuestionItem = function(label, itemtype, options) { 114 | this.label = label; 115 | this.itemtype = itemtype; 116 | this.options = $.extend({}, options); 117 | if (!("id" in this.options)) { 118 | this.options.id = createUUID(); 119 | } 120 | this.correct = this.options.correct || false; 121 | }; 122 | QuestionItem.prototype.elem = function() { 123 | return createInputComponent(this.label, this.itemtype, this.options); 124 | }; 125 | 126 | 127 | var Question = function(jsav, qtype, questionText, options) { 128 | // TODO: support for options: mustBeAsked, maxPoints 129 | // valid types: tf, fib, mc, ms (in the future: remote) 130 | this.jsav = jsav; 131 | this.asked = false; 132 | this.choices = []; 133 | this.questionText = questionText; 134 | this.maxPoints = 1; 135 | this.achievedPoints = -1; 136 | this.qtype = qtype.toUpperCase(); 137 | this.options = options; 138 | var funcs = qTypes[this.qtype]; 139 | var that = this; 140 | $.each(funcs, function(fName, f) { 141 | that[fName] = f; 142 | }); 143 | this.init(); 144 | }; 145 | var qproto = Question.prototype; 146 | qproto.id = function(newId) { 147 | if (typeof newId !== "undefined") { 148 | this.id = newId; 149 | } else { 150 | return this.id; 151 | } 152 | }; 153 | qproto.show = JSAV.anim(function() { 154 | // once asked, ignore; when recording, ignore 155 | if (this.asked || !this.jsav._shouldAnimate() || !this.jsav.options.showQuestions) { 156 | return; 157 | } 158 | this.jsav.disableControls(); 159 | this.asked = true; // mark asked 160 | var $elems = $(), 161 | that = this, 162 | i; 163 | // add feedback element 164 | $elems = $elems.add($('
      ')); 165 | // add the answer choices, randomize order 166 | var order = []; 167 | for (i=this.choices.length; i--; ) { 168 | order.push(i); 169 | } 170 | for (i=5*order.length; i--; ) { 171 | var rand1 = JSAV.utils.rand.numKey(0, order.length + 1), 172 | rand2 = JSAV.utils.rand.numKey(0, order.length + 1), 173 | tmp = order[rand1]; 174 | order[rand1] = order[rand2]; 175 | order[rand1] = tmp; 176 | } 177 | for (i=0; i < this.choices.length; i++) { 178 | $elems = $elems.add(this.choices[order[i]].elem()); 179 | } 180 | // ... and close button 181 | var close = $('').click( 182 | function () { 183 | that.dialog.close(); 184 | }); 185 | close.css("display", "none"); 186 | $elems = $elems.add(close); 187 | // .. and submit button 188 | var submit = $('').click( 189 | function () { 190 | var logData = that.feedback($elems); 191 | logData.question = that.questionText; 192 | logData.type = "jsav-question-answer"; 193 | if (that.options.id) { 194 | logData.questionId = that.options.id; 195 | } 196 | that.jsav.logEvent(logData); 197 | if (!that.jsav.options.questionResubmissionAllowed) { 198 | submit.remove(); 199 | close.show(); 200 | } 201 | }); 202 | $elems = $elems.add(submit); 203 | // .. create a close callback handler for logging the close 204 | var closeCallback = function() { 205 | var logData = { 206 | type: "jsav-question-close", 207 | question: that.questionText 208 | }; 209 | that.jsav.enableControls(); 210 | if (that.options.id) { logData.questionId = that.options.id; } 211 | that.jsav.logEvent(logData); 212 | }; 213 | var dialogClass = this.jsav.options.questionDialogClass || ""; 214 | // .. and finally create a dialog to show the question 215 | this.dialog = JSAV.utils.dialog($elems, {title: this.questionText, 216 | dialogRootElement: this.jsav.options.questionDialogBase, 217 | closeCallback: closeCallback, 218 | closeOnClick: false, 219 | dialogClass: dialogClass 220 | }); 221 | this.dialog.filter(".jsavdialog").addClass("jsavquestiondialog"); 222 | 223 | // log the question show and the choices 224 | var logChoices = []; 225 | for (i = 0; i < this.choices.length; i++) { 226 | var c = this.choices[i]; 227 | logChoices.push({label: c.label, correct: c.correct}); 228 | } 229 | var logData = { 230 | type: "jsav-question-show", 231 | question: this.questionText, 232 | questionType: this.qtype, 233 | choices: logChoices 234 | }; 235 | if (this.options.id) { logData.questionId = this.options.id; } 236 | this.jsav.logEvent(logData); 237 | 238 | return $elems; 239 | }); 240 | qproto.choiceById = function(qiId) { 241 | for (var i = this.choices.length; i--; ) { 242 | if (this.choices[i].options.id === qiId) { 243 | return this.choices[i]; 244 | } 245 | } 246 | return null; 247 | }; 248 | 249 | // dummy function for the animation, there is no need to change the state 250 | // when moving in animation; once shown, the question is not shown again 251 | qproto.state = function() {}; 252 | 253 | // add dummy function for the stuff that question types need to overwrite 254 | var noop = function() {}; 255 | $.each(['init', 'feedback', 'addChoice'], function(index, val) { 256 | qproto[val] = noop; 257 | }); 258 | 259 | // A "class" for showing questions in iframes during a slideshow 260 | var QuestionFrame = function(jsav, url, options) { 261 | this.jsav = jsav; 262 | this.url = url; 263 | this.options = options; 264 | this._showed = false; 265 | }; 266 | var qfproto = QuestionFrame.prototype; 267 | qfproto._createElement = function() { 268 | var $iframe = $(""); 269 | $iframe.prop("seamless", true); // make it seamless by default 270 | if (this.options.attr) { // pass attributes to the iframe 271 | $iframe.attr(this.options.attr); 272 | } 273 | $iframe.addClass("jsavquestionframe"); 274 | return $iframe; 275 | }; 276 | // JSAV animated show operation 277 | qfproto.show = JSAV.anim(function() { 278 | // if already showed or shouldn't show animations, return 279 | if (this._showed || !this.jsav._shouldAnimate() || !this.jsav.options.showQuestions) { 280 | return; 281 | } 282 | this._showed = true; 283 | var $iframe = this.options.element || this._createElement(); 284 | // by default, dialog shouldn't close when clicking outside of it 285 | var opts = $.extend({closeOnClick: false}, this.options); 286 | delete opts.attr; 287 | var that = this; 288 | // .. create a close callback handler for logging the close 289 | opts.closeCallback = function() { 290 | var logData = { 291 | type: "jsav-question-closeiframe", 292 | question: that.questionText 293 | }; 294 | if (that.options.id) { logData.questionId = that.options.id; } 295 | that.jsav.logEvent(logData); 296 | }; 297 | JSAV.utils.dialog($iframe, opts); 298 | var logData = { 299 | type: "jsav-question-showiframe", 300 | url: this.url 301 | }; 302 | if (opts.id) { logData.questionId = opts.id; } 303 | this.jsav.logEvent(logData); 304 | }); 305 | // dummy function for the animation, there is no need to change the state 306 | // when moving in animation; once shown, the question frame is not shown again 307 | qfproto.state = function() {}; 308 | 309 | JSAV.ext.question = function(qtype, questionText, options) { 310 | // if the question setting hasn't been added, add it now 311 | if (!this._questionSetting && this.settings) { 312 | this._questionSetting = true; 313 | this.settings.add(questionSetting(this)); 314 | } 315 | var logData = { 316 | type: "jsav-question-created", 317 | question: questionText 318 | }; 319 | this.logEvent(logData); 320 | if (qtype === "IFRAME") { 321 | return new QuestionFrame(this, questionText, options); 322 | } else { 323 | return new Question(this, qtype, questionText, $.extend({}, options)); 324 | } 325 | }; 326 | // Resets the flags of whether pop-up quetions questions have been asked already. 327 | // As a side effect, this rewinds the animation to the beginning 328 | JSAV.ext.resetQuestionAnswers = function() { 329 | // rewind the animation 330 | this.begin(); 331 | // go through all the animations steps.. 332 | for (var i = 0; i < this._redo.length; i++) { 333 | // ..and all the operations in all the steps 334 | var stepOperations = this._redo[i].operations; 335 | for (var j = 0; j < stepOperations.length; j++) { 336 | // if the target object of the operation is a question 337 | var obj = stepOperations[j].obj; 338 | if (obj && obj instanceof Question) { 339 | // set the question as not asked 340 | obj.asked = false; 341 | } 342 | } 343 | } 344 | }; 345 | JSAV.init(function() { 346 | // default to true for showing questions 347 | if (typeof this.options.showQuestions === "undefined") { 348 | this.options.showQuestions = true; 349 | } 350 | // bind the jsav-question-reset event of the container to reset the questions 351 | this.container.bind({"jsav-question-reset": function() { 352 | this.resetQuestionAnswers(); 353 | }.bind(this)}); 354 | }); 355 | }(jQuery)); -------------------------------------------------------------------------------- /src/effects.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module that contains interaction helpers for JSAV. 3 | * Depends on core.js, anim.js 4 | */ 5 | /*global JSAV, jQuery */ 6 | (function($) { 7 | "use strict"; 8 | 9 | // to use jQuery queue (code borrowed from https://github.com/rstacruz/jquery.transit/) 10 | function callOrQueue(self, queue, fn) { 11 | if (queue === true) { 12 | self.queue(fn); 13 | } else if (queue) { 14 | self.queue(queue, fn); 15 | } else { 16 | fn(); 17 | } 18 | } 19 | 20 | $.fn.extend({ 21 | // use CSS3 animations to animate the class toggle (if supported by browser) 22 | // based on https://github.com/rstacruz/jquery.transit/ 23 | jsavToggleClass: function(classNames, opts) { 24 | var self = this; 25 | 26 | var opt = $.extend({queue: true, easing: "linear", delay: 0}, opts); 27 | opt.duration = jQuery.fx.off ? 0 : typeof opt.duration === "number" ? opt.duration : 28 | opt.duration in jQuery.fx.speeds ? jQuery.fx.speeds[ opt.duration ] : jQuery.fx.speeds._default; 29 | // Account for aliases (`in` => `ease-in`). 30 | if ($.cssEase[opt.easing]) { opt.easing = $.cssEase[opt.easing]; } 31 | 32 | // Build the duration/easing/delay attributes for the transition. 33 | var transitionValue = 'all ' + opt.duration + 'ms ' + opt.easing; 34 | if (opt.delay) { transitionValue += ' ' + opt.delay + 'ms'; } 35 | 36 | // If there's nothing to do... 37 | if (opt.duration === 0) { 38 | return this.toggleClass( this, arguments ); 39 | } 40 | 41 | var RUN_DONE = false; // keep track if the toggle has already been done 42 | var run = function(nextCall) { 43 | var bound = false; // if transitionEnd was bound or not 44 | var called = false; // if callback has been called; to prevent timeout calling it again 45 | 46 | // Prepare the callback. 47 | var cb = function() { 48 | if (called) { return; } 49 | called = true; 50 | if (bound) { self.unbind($.support.transitionEnd, cb); } 51 | 52 | self.each(function() { 53 | // clear the transition properties of all elements 54 | this.style[$.support.transition] = null; 55 | }); 56 | if (typeof opt.complete === 'function') { opt.complete.apply(self); } 57 | if (typeof nextCall === 'function') { nextCall(); } 58 | }; 59 | 60 | if ((opt.duration > 0) && ($.support.transitionEnd) && ($.transit.useTransitionEnd)) { 61 | // Use the 'transitionend' event if it's available. 62 | bound = true; 63 | self.bind($.support.transitionEnd, cb); 64 | } 65 | 66 | // Fallback to timers if the 'transitionend' event isn't supported or fails to trigger. 67 | window.setTimeout(cb, opt.duration); 68 | 69 | if (!RUN_DONE) { // Apply only once 70 | // Apply transitions 71 | self.each(function() { 72 | if (opt.duration > 0) { 73 | this.style[$.support.transition] = transitionValue; 74 | } 75 | $(this).toggleClass(classNames); 76 | }); 77 | } 78 | RUN_DONE = true; 79 | }; 80 | 81 | // Defer running. This allows the browser to paint any pending CSS it hasn't 82 | // painted yet before doing the transitions. 83 | var deferredRun = function(next) { 84 | this.offsetWidth; // force a repaint 85 | run(next); 86 | }; 87 | 88 | // Use jQuery's fx queue. 89 | callOrQueue(self, opt.queue, deferredRun); 90 | 91 | // Chainability. 92 | return this; 93 | } 94 | }); 95 | 96 | 97 | 98 | var parseValueEffectParameters = function() { 99 | // parse the passed arguments 100 | // possibilities are: 101 | // - array, ind, array, ind 102 | // - array, ind, node 103 | // - node, array, ind 104 | // - node, node 105 | var params = { args1: [], 106 | args2: [], 107 | from: arguments[0] // first param is always 1st structure 108 | }; 109 | var secondstrPos = 1; 110 | if (typeof arguments[1] === "number") { // array index 111 | params.args1 = [ arguments[1] ]; 112 | secondstrPos = 2; // 2nd structure will be at arg index 2 113 | } 114 | params.to = arguments[secondstrPos]; 115 | if (typeof arguments[secondstrPos + 1] === "number") { // array index 116 | params.args2 = [ arguments[secondstrPos + 1] ]; 117 | } 118 | return params; 119 | }; 120 | var doValueEffect = function(opts) { 121 | // get the values of the from and to elements 122 | var from = opts.from, // cache the values 123 | to = opts.to, 124 | val = from.value.apply(from, opts.args1), 125 | oldValue = to.value.apply(to, opts.args2), 126 | $fromValElem, $toValElem, toPos; 127 | // set the value in original structure to empty string or, if undoing, the old value 128 | if (opts.mode === "swap") { 129 | from.value.apply(from, opts.args1.concat([ oldValue, {record: false} ])); 130 | } else if (opts.mode === "move" || typeof opts.old !== "undefined") { 131 | from.value.apply(from, opts.args1.concat([(typeof opts.old !== "undefined")?opts.old:"", {record: false}])); 132 | } 133 | // set the value of the target structure 134 | to.value.apply(to, opts.args2.concat([val, {record: false}])); 135 | 136 | // get the HTML elements for the values, for arrays, use the index 137 | if (from.constructor === JSAV._types.ds.AVArray) { 138 | $fromValElem = from.element.find("li:eq(" + opts.args1[0] + ") .jsavvaluelabel"); 139 | } else if (from.element.hasClass("jsavlabel")) { // special treatment for labels 140 | $fromValElem = from.element; 141 | } else { 142 | $fromValElem = from.element.find(".jsavvaluelabel"); 143 | } 144 | if (to.constructor === JSAV._types.ds.AVArray) { 145 | $toValElem = to.element.find("li:eq(" + opts.args2[0] + ") .jsavvaluelabel"); 146 | } else if (to.element.hasClass("jsavlabel")) { // special treatment for labels 147 | $toValElem = to.element; 148 | } else { 149 | $toValElem = to.element.find(".jsavvaluelabel"); 150 | } 151 | 152 | if (this._shouldAnimate()) { // only animate when playing, not when recording 153 | var toValElemHeight = $toValElem.height(); // saved into a variable, because the computed value is zero after repositioning 154 | var fromValElemHeight = $fromValElem.height(); // saved into a variable, because the computed value is zero after repositioning 155 | $toValElem.position({of: $fromValElem}); // let jqueryUI position it on top of the from element 156 | if (opts.mode === "swap") { 157 | toPos = $.extend({}, $toValElem.position()); 158 | if (to.options.layout !== "bar") { 159 | $toValElem.css({left: 0, top: 0}); 160 | } else { 161 | $toValElem.css({left: 0, bottom: 0, top: ""}); 162 | } 163 | $fromValElem.position({of: $toValElem}); 164 | $toValElem.css(toPos); 165 | if (from.options.layout !== "bar") { 166 | $fromValElem.transition({left: 0, top: 0}, this.SPEED, 'linear'); 167 | } else { 168 | var bottom = $fromValElem.parent().height() - $fromValElem.position().top - fromValElemHeight; 169 | $fromValElem.css({top: "", bottom: bottom}); 170 | $fromValElem.transition({left: 0, bottom: 0}, this.SPEED, 'linear'); // animate to final position 171 | } 172 | } 173 | if (to.options.layout !== "bar") { 174 | $toValElem.transition({left: 0, top: 0}, this.SPEED, 'linear'); // animate to final position 175 | } else { 176 | var bottom = $toValElem.parent().height() - $toValElem.position().top - toValElemHeight; 177 | $toValElem.css({top: "", bottom: bottom}); 178 | $toValElem.transition({left: 0, bottom: 0}, this.SPEED, 'linear'); // animate to final position 179 | } 180 | } 181 | 182 | // return "reversed" parameters and the old value for undoing 183 | return [ { 184 | from: to, 185 | args1: opts.args2, 186 | to: from, 187 | args2: opts.args1, 188 | old: oldValue, 189 | mode: opts.mode 190 | } ]; 191 | }; 192 | 193 | JSAV.ext.effects = { 194 | /* toggles the clazz class of the given elements with CSS3 transitions */ 195 | _toggleClass: function($elems, clazz, options) { 196 | this._animations += $elems.length; 197 | var that = this; 198 | 199 | $elems.jsavToggleClass(clazz, {duration: (options && options.duration) || this.SPEED, delay: (options && options.delay) || 0, 200 | complete: function() { that._animations--; } 201 | }); 202 | }, 203 | /* Animate the properties of the given elements with CSS3 transitions */ 204 | transition: function($elems, cssProps, options) { 205 | this._animations += $elems.length; 206 | var that = this; 207 | $elems.transition(cssProps, {duration: (options && options.duration) || this.SPEED, 208 | delay: (options && options.delay) || 0, 209 | complete: function() { that._animations--; } 210 | }); 211 | }, 212 | /* toggles visibility of an element */ 213 | _toggleVisible: function() { 214 | if (this.jsav._shouldAnimate()) { // only animate when playing, not when recording 215 | this.element.fadeToggle(this.jsav.SPEED); 216 | } else { 217 | this.element.toggle(); 218 | } 219 | return []; 220 | }, 221 | /* shows an element */ 222 | show: function(options) { 223 | if (this.element.filter(":visible").size() === 0) { 224 | this._toggleVisible(options); 225 | } 226 | return this; 227 | }, 228 | /* hides an element */ 229 | hide: function(options) { 230 | if (this.element.filter(":visible").size() > 0) { 231 | this._toggleVisible(options); 232 | } 233 | return this; 234 | }, 235 | copyValue: function() { 236 | var params = parseValueEffectParameters.apply(null, arguments); 237 | // wrap the doValueEffect function to JSAV animatable function 238 | JSAV.anim(doValueEffect).call(this, params); 239 | }, 240 | moveValue: function() { 241 | var params = parseValueEffectParameters.apply(null, arguments); 242 | params.mode = "move"; 243 | // wrap the doValueEffect function to JSAV animatable function 244 | JSAV.anim(doValueEffect).call(this, params); 245 | }, 246 | swapValues: function() { 247 | var params = parseValueEffectParameters.apply(null, arguments); 248 | params.mode = "swap"; 249 | // wrap the doValueEffect function to JSAV animatable function 250 | JSAV.anim(doValueEffect).call(this, params); 251 | }, 252 | swap: function($str1, $str2, options) { 253 | var opts = $.extend({translateY: true, arrow: true, highlight: true, swapClasses: false}, options), 254 | $val1 = $str1.find("span.jsavvalue"), 255 | $val2 = $str2.find("span.jsavvalue"), 256 | classes1 = $str1.attr("class"), 257 | classes2 = $str2.attr("class"), 258 | posdiffX = JSAV.position($str1).left - JSAV.position($str2).left, 259 | posdiffY = opts.translateY?JSAV.position($str1).top - JSAV.position($str2).top:0, 260 | $both = $($str1).add($str2), 261 | speed = this.SPEED/5; 262 | 263 | // ..swap the value elements... 264 | var val1 = $val1[0], 265 | val2 = $val2[0], 266 | aparent = val1.parentNode, 267 | asibling = val1.nextSibling===val2 ? val1 : val1.nextSibling; 268 | val2.parentNode.insertBefore(val1, val2); 269 | aparent.insertBefore(val2, asibling); 270 | 271 | // ... and swap classes... 272 | if (opts.swapClasses) { 273 | $str1.attr("class", classes2); 274 | $str2.attr("class", classes1); 275 | } 276 | 277 | // ..and finally animate.. 278 | if (this._shouldAnimate()) { // only animate when playing, not when recording 279 | if ('Raphael' in window && opts.arrow) { // draw arrows only if Raphael is loaded 280 | var off1 = $val1.offset(), 281 | off2 = $val2.offset(), 282 | coff = this.canvas.offset(), 283 | x1 = off1.left - coff.left + $val1.outerWidth()/2, 284 | x2 = off2.left - coff.left + $val2.outerWidth()/2, 285 | y1 = off1.top - coff.top + $val1.outerHeight() + 5, 286 | y2 = y1, 287 | curve = 20, 288 | cx1 = x1, 289 | cx2 = x2, 290 | cy1 = y2 + curve, 291 | cy2 = y2 + curve, 292 | arrowStyle = "classic-wide-long"; 293 | if (posdiffY > 1 || posdiffY < 1) { 294 | y2 = off2.top - coff.top + $val2.outerHeight() + 5; 295 | var angle = (y2 - y1) / (x2 - x1), 296 | c1 = Math.pow(y1, 2) - (curve*curve / (1 + angle*angle)), 297 | c2 = Math.pow(y2, 2) - (curve*curve / (1 + angle*angle)); 298 | cy1 = y1 + Math.sqrt(y1*y1 - c1); 299 | cx1 = x1 - angle*Math.sqrt(y1*y1 - c1); 300 | cy2 = y2 + Math.sqrt(y2*y2 - c2); 301 | cx2 = x2 - angle*Math.sqrt(y2*y2 - c2); 302 | } 303 | // .. and draw a curved path with arrowheads 304 | var arr = this.getSvg().path("M" + x1 + "," + y1 + "C" + cx1 + "," + cy1 + " " + cx2 + "," + cy2 + " " + x2 + "," + y2).attr({"arrow-start": arrowStyle, "arrow-end": arrowStyle, "stroke-width": 5, "stroke":"lightGray"}); 305 | } 306 | // .. then set the position so that the array appears unchanged.. 307 | $val2.css({"x": -posdiffX, "y": -posdiffY, z: 1}); 308 | $val1.css({"x": posdiffX, "y": posdiffY, z: 1}); 309 | 310 | // mark to JSAV that we're animating something more complex 311 | this._animations += 1; 312 | var jsav = this; 313 | // .. animate the color .. 314 | if (opts.highlight) { 315 | $both.addClass("jsavswap", 3*speed); 316 | } 317 | // ..animate the translation to 0, so they'll be in their final positions.. 318 | $val1.transition({"x": 0, y: 0, z: 0}, 7*speed, 'linear'); 319 | $val2.transition({x: 0, y: 0, z: 0}, 7*speed, 'linear', 320 | function() { 321 | if (arr) { arr.remove(); } // ..remove the arrows if they exist 322 | // ..and finally animate to the original styles. 323 | if (opts.highlight) { 324 | $both.removeClass("jsavswap", 3*speed); 325 | } 326 | // notify jsav that we're done with our animation 327 | jsav._animations -= 1; 328 | }); 329 | } 330 | } 331 | }; 332 | }(jQuery)); -------------------------------------------------------------------------------- /src/anim.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module that contains the animator implementations. 3 | * Depends on core.js 4 | */ 5 | /*global JSAV, jQuery */ 6 | (function($) { 7 | "use strict"; 8 | 9 | var DEFAULT_SPEED = 300, 10 | playingCl = "jsavplaying"; // class used to mark controls when playing 11 | 12 | 13 | if (typeof JSAV === "undefined") { return; } 14 | 15 | var AnimatableOperation = function(opts) { 16 | this.obj = opts.obj; 17 | this.effect = opts.effect; 18 | this.args = opts.args; 19 | if (opts.undo) { 20 | this.undoeffect = opts.undo; 21 | } 22 | if (opts.undoargs) { 23 | this.undoArgs = opts.undoargs; 24 | } 25 | }; 26 | AnimatableOperation.prototype.apply = function() { 27 | var self = this; 28 | var obj = self.obj, 29 | state = obj.state?obj.state():null; 30 | var retVal = this.effect.apply(this.obj, this.args); 31 | if (typeof retVal === "undefined" || retVal === this.obj) { 32 | if (typeof this.undoeffect === "undefined" || !$.isFunction(this.undoeffect)) { 33 | this.undoeffect = (function() { 34 | return function() { // we create one that will set the state of obj to its current state 35 | obj.state(state); 36 | }; 37 | }()); 38 | } 39 | } else { 40 | this.undoArgs = retVal; 41 | } 42 | }; 43 | AnimatableOperation.prototype.undo = function() { 44 | if (typeof this.undoArgs === "undefined") { 45 | this.undoeffect.apply(this.obj, this.args); 46 | } else { 47 | this.effect.apply(this.obj, this.undoArgs); 48 | } 49 | }; 50 | 51 | var AnimStep = function(options) { 52 | this.operations = []; 53 | this.options = options || {}; 54 | }; 55 | AnimStep.prototype.add = function(oper) { 56 | this.operations.push(oper); 57 | }; 58 | AnimStep.prototype.isEmpty = function() { 59 | return this.operations.length === 0; 60 | }; 61 | 62 | function backward(filter) { 63 | if (this._undo.length === 0) { return; } 64 | var step = this._undo.pop(); 65 | var ops = step.operations; // get the operations in the step we're about to undo 66 | for (var i = ops.length - 1; i >= 0; i--) { // iterate the operations 67 | // operation contains: [target object, effect function, arguments, undo function] 68 | var prev = ops[i]; 69 | prev.undo(); 70 | } 71 | this._redo.unshift(step); 72 | // if a filter function is given, check if this step matches 73 | // if not, continue moving backward 74 | if (filter && $.isFunction(filter) && !filter(step)) { 75 | this.backward(filter); 76 | } 77 | // trigger an event on the container to update the counter 78 | this.container.trigger("jsav-updatecounter", [this.currentStep() + 1, this.totalSteps() + 1]); 79 | return step; 80 | } 81 | 82 | function forward() { 83 | if (this._redo.length === 0) { return; } 84 | var step = this._redo.shift(); 85 | var ops = step.operations; // get the operations in the step we're about to undo 86 | for (var i = 0; i < ops.length; i++) { 87 | var next = ops[i]; 88 | next.apply(); 89 | } 90 | this._undo.push(step); 91 | // trigger an event on the container to update the counter 92 | this.container.trigger("jsav-updatecounter", [this.currentStep() + 1, this.totalSteps() + 1]); 93 | return step; // return the just applied step 94 | } 95 | 96 | function begin() { 97 | var oldFx = $.fx.off || false; 98 | $.fx.off = true; 99 | while (this._undo.length) { 100 | this.backward(); 101 | } 102 | $.fx.off = oldFx; 103 | return this; 104 | } 105 | 106 | function end() { 107 | var oldFx = $.fx.off || false; 108 | $.fx.off = true; 109 | while (this._redo.length) { 110 | this.forward(); 111 | } 112 | $.fx.off = oldFx; 113 | return this; 114 | } 115 | 116 | 117 | JSAV.init(function() { 118 | this._animations = 0; 119 | this._redo = []; // stack for operations to redo 120 | this._undo = []; // stack for operations to undo 121 | var that = this, 122 | $controls = $(".jsavcontrols", this.container); 123 | 124 | function logAnimEvent(action) { 125 | var eventData = { 126 | "type": action, 127 | "currentStep": that.currentStep(), 128 | "totalSteps": that.totalSteps() 129 | }; 130 | that.logEvent(eventData); 131 | } 132 | 133 | // set a timer to remove the class indicating animation playing 134 | // once the animation is completed. optional callback function that 135 | // will be called once done. 136 | function clearPlayingTimeout(jsav, callback) { 137 | var timerid; 138 | var timeouter = function() { 139 | if (!jsav.isAnimating()) { 140 | jsav.container.removeClass(playingCl); 141 | jsav._animations = 0; 142 | if ($.isFunction(callback)) { 143 | callback(); 144 | } 145 | clearInterval(timerid); 146 | } 147 | }; 148 | timerid = setInterval(timeouter, 50); 149 | } 150 | 151 | // function for clearing the playing flag 152 | this._clearPlaying = function clearPlaying(callback) { 153 | if (this.isAnimating()) { 154 | clearPlayingTimeout(this, callback); 155 | } else { 156 | this.container.removeClass(playingCl); 157 | if ($.isFunction(callback)) { 158 | callback(); 159 | } 160 | } 161 | }; 162 | // reqister event handlers for the control buttons 163 | var beginHandler = function(e) { 164 | e.preventDefault(); 165 | e.stopPropagation(); 166 | // if playing flag is set, don't respond 167 | if (that.container.hasClass(playingCl)) { return; } 168 | // set the playing flag, that is, a class on the controls 169 | that.container.addClass(playingCl); 170 | that.begin(); // go to beginning 171 | that._clearPlaying(); // clear the flag 172 | // log the event 173 | logAnimEvent("jsav-begin"); 174 | }; 175 | var backwardHandler = function(e, filter) { 176 | e.preventDefault(); 177 | e.stopPropagation(); 178 | e.stopPropagation(); 179 | if (that.container.hasClass(playingCl)) { return; } 180 | that.container.addClass(playingCl); 181 | that.backward(filter); 182 | // clear playing flag after a timeout for animations to end 183 | that._clearPlaying(); 184 | // log the event 185 | logAnimEvent("jsav-backward"); 186 | }; 187 | var forwardHandler = function(e, filter) { 188 | e.preventDefault(); 189 | e.stopPropagation(); 190 | if (that.container.hasClass(playingCl)) { return; } 191 | that.container.addClass(playingCl); 192 | that.forward(filter); 193 | that._clearPlaying(); 194 | // log the event 195 | logAnimEvent("jsav-forward"); 196 | }; 197 | var endHandler = function(e) { 198 | e.preventDefault(); 199 | e.stopPropagation(); 200 | if (that.container.hasClass(playingCl)) { return; } 201 | that.container.addClass(playingCl); 202 | that.end(); 203 | that._clearPlaying(); 204 | // log the event 205 | logAnimEvent("jsav-end"); 206 | }; 207 | if ($controls.size() !== 0) { 208 | var tmpTranslation = this._translate("beginButtonTitle"); 209 | $("<<").click(beginHandler).appendTo($controls); 210 | tmpTranslation = this._translate("backwardButtonTitle"); 211 | $("<").click(backwardHandler).appendTo($controls); 212 | tmpTranslation = this._translate("forwardButtonTitle"); 213 | $(">").click(forwardHandler).appendTo($controls); 214 | tmpTranslation = this._translate("endButtonTitle"); 215 | $(">>").click(endHandler).appendTo($controls); 216 | this._controlsContainer = $controls; 217 | } 218 | // bind the handlers to events to enable control by triggering events 219 | this.container.bind({ "jsav-forward": forwardHandler, 220 | "jsav-backward": backwardHandler, 221 | "jsav-begin": beginHandler, 222 | "jsav-end": endHandler }); 223 | 224 | // add slideshow counter if an element with class counter exists 225 | var counter = $(".jsavcounter", this.container); 226 | // register an event to be triggered on container to update the counter 227 | if (counter.size() > 0) { 228 | counter.text("0 / 0"); // initialize the counter text 229 | // register event handler to update the counter 230 | this.container.bind("jsav-updatecounter", function(evet, current, total) { 231 | counter.text(current + " / " + total); 232 | }); 233 | } 234 | 235 | // register a listener for the speed change event 236 | $(document).bind("jsav-speed-change", function(e, args) { 237 | that.SPEED = args; 238 | }); 239 | }); 240 | 241 | // this function can be used to "decorate" effects to be applied when moving forward 242 | // in the animation 243 | function anim(effect, undo) { 244 | // returns a function that can be used to provide function calls that are applied later 245 | // when viewing the visualization 246 | return function() { 247 | var jsav = this; // this points to the objects whose function was decorated 248 | var args = $.makeArray(arguments), 249 | norecord = false; 250 | if (args.length > 0 && args[args.length-1] && typeof args[args.length-1] === "object" && 251 | args[args.length-1].record === false) { 252 | norecord = true; 253 | } 254 | if (!jsav.hasOwnProperty("_redo")) { jsav = this.jsav; } 255 | if (jsav.options.animationMode === 'none' || norecord) { // if not recording, apply immediately 256 | effect.apply(this, arguments); 257 | } else { 258 | var stackTop = jsav._undo[jsav._undo.length - 1]; 259 | if (!stackTop) { 260 | stackTop = new AnimStep(); 261 | jsav._undo.push(stackTop); 262 | } 263 | // add to stack: [target object, effect function, arguments, undo function] 264 | var oper = new AnimatableOperation({obj: this, effect: effect, 265 | args: arguments, undo: undo}); 266 | stackTop.add(oper); 267 | if (jsav._shouldAnimate()) { 268 | jsav.container.addClass(playingCl); 269 | } 270 | oper.apply(); 271 | if (jsav._shouldAnimate()) { 272 | jsav._clearPlaying(); 273 | } 274 | } 275 | return this; 276 | }; 277 | } 278 | function moveWrapper(func, filter) { 279 | var origStep = this.currentStep(), 280 | step = func.call(this); 281 | if (!step) { 282 | return false; 283 | } 284 | if (filter) { 285 | if ($.isFunction(filter)) { 286 | var filterMatch = filter(step), 287 | matched = filterMatch; 288 | while (!filterMatch && this.currentStep() < this.totalSteps()) { 289 | step = func.call(this); 290 | if (!step) { break; } 291 | filterMatch = filter(step); 292 | matched = matched || filterMatch; 293 | } 294 | if (!matched) { 295 | this.jumpToStep(origStep); 296 | return false; 297 | } 298 | } 299 | } 300 | return true; 301 | } 302 | JSAV.anim = anim; 303 | if (typeof localStorage !== 'undefined' && localStorage) { // try to fetch a stored setting for speed from localStorage 304 | var spd = localStorage.getItem("jsav-speed"); 305 | if (spd) { // if we have a value, it is a string (from localStorage) 306 | spd = parseInt(spd, 10); 307 | if (isNaN(spd)) { // if we couldn't parse an int, fallback to default speed 308 | spd = DEFAULT_SPEED; 309 | } 310 | } else { // no value in localStorage, go with the default speed 311 | spd = DEFAULT_SPEED; 312 | } 313 | JSAV.ext.SPEED = spd; 314 | } else { 315 | JSAV.ext.SPEED = DEFAULT_SPEED; 316 | } 317 | JSAV.ext.begin = begin; 318 | JSAV.ext.end = end; 319 | JSAV.ext.forward = function(filter) { 320 | return moveWrapper.call(this, forward, filter); 321 | }; 322 | JSAV.ext.backward = function(filter) { 323 | return moveWrapper.call(this, backward, filter); 324 | }; 325 | JSAV.ext.currentStep = function() { 326 | return this._undo.length; 327 | }; 328 | JSAV.ext.totalSteps = function() { 329 | return this._undo.length + this._redo.length; 330 | }; 331 | JSAV.ext.animInfo = function() { 332 | // get some "size" info about the animation, namely the number of steps 333 | // and the total number of effects (or operations) in the animation 334 | var info = { steps: this.totalSteps()}, 335 | i, 336 | effects = 0; 337 | for (i = this._undo.length; i--; ) { 338 | effects += this._undo[i].operations.length; 339 | } 340 | for (i = this._redo.length; i--; ) { 341 | effects += this._redo[i].operations.length; 342 | } 343 | info.effects = effects; 344 | return info; 345 | }; 346 | JSAV.ext.step = function(options) { 347 | var updateRelative = (options && options.updateRelative === false ? false : true); 348 | if (updateRelative) { this.container.trigger("jsav-updaterelative"); } 349 | if (this.options.animationMode !== "none") { 350 | this._undo.push(new AnimStep(options)); // add new empty step to oper. stack 351 | if (options && this.message && options.message) { 352 | this.message(options.message); 353 | } 354 | } 355 | return this; 356 | }; 357 | JSAV.ext.clearAnimation = function(options) { 358 | var opts = $.extend({undo: true, redo: true}, options); 359 | if (opts.undo) { 360 | this._undo = []; 361 | } 362 | if (opts.redo) { 363 | this._redo = []; 364 | } 365 | }; 366 | JSAV.ext.displayInit = function() { 367 | this.container.trigger("jsav-updaterelative"); 368 | this.clearAnimation({redo: false}); 369 | return this; 370 | }; 371 | /** Jumps to step number step. */ 372 | JSAV.ext.jumpToStep = function(step) { 373 | var stepCount = this.totalSteps(), 374 | jsav = this, 375 | stepFunction = function(stp) { 376 | return jsav.currentStep() === step; 377 | }; 378 | var oldFx = $.fx.off || false; 379 | $.fx.off = true; 380 | if (step >= stepCount) { 381 | this.end(); 382 | } else if (step < 0) { 383 | this.begin(); 384 | } else if (step < this.currentStep()) { 385 | this.backward(stepFunction); 386 | } else { 387 | this.forward(stepFunction); 388 | } 389 | $.fx.off = oldFx; 390 | return this; 391 | }; 392 | JSAV.ext.stepOption = function(name, value) { 393 | var step = this._undo[this._undo.length - 1]; 394 | if (value !== undefined) { // set named property 395 | if (step) { 396 | step.options[name] = value; 397 | } 398 | } else if (typeof name === "string") { // get named property 399 | if (step) { 400 | return step.options[name]; 401 | } else { 402 | return undefined; 403 | } 404 | } else { // assume an object 405 | for (var item in name) { 406 | if (name.hasOwnProperty(item)) { 407 | this.stepOption(item, name[item]); 408 | } 409 | } 410 | } 411 | }; 412 | JSAV.ext.recorded = function() { 413 | // if there are more than one step, and the last step is empty, remove it 414 | if (this._undo.length > 1 && this._undo[this._undo.length - 1].isEmpty()) { 415 | this._undo.pop(); 416 | } else { 417 | this.container.trigger("jsav-updaterelative"); 418 | } 419 | this.begin(); 420 | this.RECORD = false; 421 | $.fx.off = false; 422 | this.logEvent({type: "jsav-recorded"}); 423 | return this; 424 | }; 425 | JSAV.ext.isAnimating = function() { 426 | // returns true if animation is playing, false otherwise 427 | return !!this.container.find(":animated").size() || this._animations > 0; 428 | }; 429 | JSAV.ext._shouldAnimate = function() { 430 | return (!this.RECORD && !$.fx.off && this.SPEED > 50); 431 | }; 432 | JSAV.ext.disableControls = function() { 433 | if (this._controlsContainer) { 434 | this._controlsContainer.addClass("jsavdisabled"); 435 | } 436 | }; 437 | JSAV.ext.enableControls = function() { 438 | if (this._controlsContainer) { 439 | this._controlsContainer.removeClass("jsavdisabled"); 440 | } 441 | }; 442 | }(jQuery)); 443 | 444 | /** Override the borderWidth/Color CSS getters to return the 445 | info for border-top. */ 446 | jQuery.cssHooks.borderColor = { 447 | get: function(elem) { 448 | return jQuery(elem).css("border-top-color"); 449 | } 450 | }; 451 | jQuery.cssHooks.borderWidth = { 452 | get: function(elem) { 453 | return jQuery(elem).css("border-top-width"); 454 | } 455 | }; --------------------------------------------------------------------------------