├── .eslintrc.json ├── .gitignore ├── src ├── media │ ├── reset.ai │ └── reset.svg ├── browserify.js ├── stand-alone.js ├── uniwheel.js ├── svg-utilities.js ├── utilities.js ├── control-icons.js └── shadow-viewport.js ├── svg-pan-zoom-logo.png ├── tslint.json ├── .editorconfig ├── demo ├── require-main.js ├── require.html ├── embed.html ├── object.html ├── img.html ├── custom-controls.html ├── resize.html ├── sinchronized.html ├── layers.html ├── thumbnailViewer.html ├── dynamic-load.html ├── limit-pan.html ├── simple-animation.html ├── custom-event-handlers.html ├── mobile.html └── thumbnailViewer.js ├── tsconfig.json ├── index.html ├── LICENSE ├── ISSUE_TEMPLATE.md ├── tests ├── index.html ├── test_typescript.ts ├── assets │ └── qunit.css └── test_api.js ├── package.json ├── gulpfile.js ├── server.js ├── dist ├── svg-pan-zoom.d.ts └── svg-pan-zoom.min.js ├── svg-pan-zoom-logo.svg └── README.md /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["plugin:prettier/recommended"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | ./server.js 3 | **/*.orig 4 | .DS_Store 5 | npm-debug.log 6 | -------------------------------------------------------------------------------- /src/media/reset.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bumbu/svg-pan-zoom/HEAD/src/media/reset.ai -------------------------------------------------------------------------------- /svg-pan-zoom-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bumbu/svg-pan-zoom/HEAD/svg-pan-zoom-logo.png -------------------------------------------------------------------------------- /src/browserify.js: -------------------------------------------------------------------------------- 1 | var SvgPanZoom = require("./svg-pan-zoom.js"); 2 | 3 | module.exports = SvgPanZoom; 4 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rules": { 4 | "no-namespace": [true, "allow-declarations"], 5 | "interface-name": false 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org/ 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 2 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /demo/require-main.js: -------------------------------------------------------------------------------- 1 | require.config({ 2 | paths: { 3 | 'svg-pan-zoom': '../dist/svg-pan-zoom' 4 | } 5 | }) 6 | 7 | require(["svg-pan-zoom"], function(svgPanZoom) { 8 | svgPanZoom('#demo-tiger', { 9 | zoomEnabled: true, 10 | controlIconsEnabled: true 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /demo/require.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | My Sample Project 5 | 6 | 7 | 8 |

Demo for svg-pan-zoom: Using Require.js

9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "UMD", 4 | "target": "es5", 5 | "diagnostics": true, 6 | "noEmit": true, 7 | "noImplicitAny": true, 8 | "noImplicitThis": true, 9 | "strictNullChecks": false, 10 | "forceConsistentCasingInFileNames": true, 11 | "baseUrl": "./", 12 | "typeRoots": [ 13 | "./" 14 | ], 15 | "types": [] 16 | }, 17 | "files": [ 18 | "dist/svg-pan-zoom.d.ts", 19 | "tests/test_typescript.ts" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /src/stand-alone.js: -------------------------------------------------------------------------------- 1 | var svgPanZoom = require("./svg-pan-zoom.js"); 2 | 3 | // UMD module definition 4 | (function(window, document) { 5 | // AMD 6 | if (typeof define === "function" && define.amd) { 7 | define("svg-pan-zoom", function() { 8 | return svgPanZoom; 9 | }); 10 | // CMD 11 | } else if (typeof module !== "undefined" && module.exports) { 12 | module.exports = svgPanZoom; 13 | 14 | // Browser 15 | // Keep exporting globally as module.exports is available because of browserify 16 | window.svgPanZoom = svgPanZoom; 17 | } 18 | })(window, document); 19 | -------------------------------------------------------------------------------- /demo/embed.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |

Demo for svg-pan-zoom: SVG in HTML 'embed' element

10 | 11 | 12 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /demo/object.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |

Demo for svg-pan-zoom: SVG in HTML 'object' element

10 | Your browser does not support SVG 11 | 12 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /demo/img.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | 12 |

Demo for svg-pan-zoom: SVG in HTML 'img' element

13 | svg image of a tiger 14 | 15 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |

Demos for svg-pan-zoom

9 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2009-2010 Andrea Leofreddi 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, this 11 | list of conditions and the following disclaimer in the documentation and/or 12 | other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 15 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 18 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 19 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 20 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 21 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 22 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 23 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /demo/custom-controls.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |

Demo for svg-pan-zoom: SVG in HTML 'object' element

10 | Your browser does not support SVG 11 |
12 | 13 | 14 | 15 |
16 | 17 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /demo/resize.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |

Demo for svg-pan-zoom: Resize SVG container on page resize

14 |
15 | 16 | 17 | 18 | 19 | 20 |
21 | 22 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Hi there! 2 | 3 | Thanks for submitting an issue to svg-pan-zoom. 4 | 5 | To help us help you better, please do the following before submitting an issue: 6 | 7 | 1. Review: 8 | * Available documentation https://github.com/bumbu/svg-pan-zoom/blob/master/README.md 9 | * Common Issues & FAQ https://github.com/bumbu/svg-pan-zoom#common-issues--faq 10 | * Existing examples https://github.com/bumbu/svg-pan-zoom#demos 11 | 1. Check if the same bug/feature request [wasn't previously reported](https://github.com/bumbu/svg-pan-zoom/issues?q=is%3Aissue%20) 12 | 1. Make sure you are not asking a usage or debugging question. If you are, use [StackOverflow](http://stackoverflow.com/questions/tagged/svgpanzoom). 13 | 1. Fill in the information that corresponds to your type of issue below 14 | 1. If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem via https://jsfiddle.net or similar (template: http://jsfiddle.net/bumbu/167usffr/). 15 | 1. Delete this intro and any unrelated text :smile: (if you do not we'll assume you haven't read these instructions and automatically close the issue) 16 | 17 | 18 | Bug report 19 | --- 20 | 21 | ### Expected behaviour 22 | _your text here_ 23 | 24 | ### Actual behaviour 25 | _your text here_ 26 | 27 | ### Steps to reproduce the behaviour 28 | 29 | 1. 30 | 2. 31 | 32 | ### Configuration 33 | 34 | - svg-pan-zoom version: `` 35 | - Browser(s): `` 36 | - Operating system(s): `` 37 | - A relevant example URL: 38 | 39 | 40 | Feature Request 41 | --- 42 | 43 | - Feature description 44 | - Reasons for adopting new feature 45 | - Is this a breaking change? (How will this affect existing functionality) 46 | -------------------------------------------------------------------------------- /demo/sinchronized.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |

Demo for svg-pan-zoom: Sinchronized pan and zoom

10 | 11 | 12 | 13 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /demo/layers.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |

Demo for svg-pan-zoom: Zooming just one SVG layer

10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
32 | 33 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /demo/thumbnailViewer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 48 | 49 | 50 | 51 | 52 |
53 | 54 |
55 | 56 |
57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 |
66 | 67 | 68 | 69 | 70 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /demo/dynamic-load.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |

Demo for svg-pan-zoom: Dynamic SVG load

11 |
12 | 13 | 14 | 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Tests 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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svg-pan-zoom", 3 | "version": "3.6.2", 4 | "main": "dist/svg-pan-zoom.js", 5 | "module": "src/svg-pan-zoom.js", 6 | "types": "dist/svg-pan-zoom.d.ts", 7 | "browser": "src/browserify.js", 8 | "license": "BSD-2-Clause", 9 | "description": "JavaScript library for panning and zooming an SVG image from the mouse, touches and programmatically.", 10 | "scripts": { 11 | "start": "gulp", 12 | "lint": "tslint 'dist/svg-pan-zoom.d.ts'", 13 | "test": "tsc && gulp test", 14 | "build": "gulp build", 15 | "server": "node server.js" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/bumbu/svg-pan-zoom" 20 | }, 21 | "bugs": { 22 | "url": "https://github.com/bumbu/svg-pan-zoom/issues" 23 | }, 24 | "contributors": [ 25 | "Andrea Leofreddi ", 26 | "Anders Riutta ", 27 | "Zeng Xiaohui", 28 | "Barry Coughlan (https://github.com/bcoughlan)", 29 | "Risingson", 30 | "bumbu alex (http://bumbu.me>)", 31 | "Alexander Pico ", 32 | "Kyran Burraston (http://kyranburraston.co.uk)", 33 | "Risingson (https://github.com/Risingson)", 34 | "Siddhanathan Shanmugam ", 35 | "Karina Simard (https://github.com/ksimard)", 36 | "Christopher Clark ", 37 | "Vladimir Prus (http://vladimirprus.com)", 38 | "Barry Coughlan (https://github.com/bcoughlan)", 39 | "Ionică Bizău (http://ionicabizau.net/)", 40 | "Ciprian Placintă (https://github.com/CiprianPlacinta)", 41 | "Riccardo Santoro (https://github.com/VeNoMiS)", 42 | "César Vidril (https://github.com/Yimiprod)", 43 | "Androl Genhald (https://github.com/AndrolGenhald)", 44 | "James Newell (https://github.com/musicfuel)", 45 | "KoenkookpotPlasmans (https://github.com/KoenkookpotPlasmans)", 46 | "Hassan Shaikley (http://hassanshaikley.github.io/)" 47 | ], 48 | "keywords": [ 49 | "svg", 50 | "pan", 51 | "zoom" 52 | ], 53 | "devDependencies": { 54 | "browserify": "^16.5.0", 55 | "eslint-config-prettier": "^6.5.0", 56 | "eslint-plugin-prettier": "^3.1.1", 57 | "gulp": "^4.0.2", 58 | "gulp-eslint": "^6.0.0", 59 | "gulp-header": "^2.0.9", 60 | "gulp-if": "^2.0.2", 61 | "gulp-qunit": "^2.1.0", 62 | "gulp-rename": "^1.4.0", 63 | "gulp-uglify": "^3.0.2", 64 | "prettier": "^1.18.2", 65 | "qunit": "^2.9.3", 66 | "qunitjs": "npm:qunit@^2.9.3", 67 | "tslint": "^4.5.1", 68 | "typescript": "^3.6.4", 69 | "vinyl-buffer": "^1.0.1", 70 | "vinyl-source-stream": "^2.0.0" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Modules 3 | */ 4 | var gulp = require("gulp"), 5 | uglify = require("gulp-uglify"), 6 | browserify = require("browserify"), 7 | source = require("vinyl-source-stream"), 8 | rename = require("gulp-rename"), 9 | qunit = require("gulp-qunit"), 10 | eslint = require("gulp-eslint"), 11 | gulpIf = require("gulp-if"), 12 | header = require("gulp-header"), 13 | buffer = require("vinyl-buffer"), 14 | pkg = require("./package.json"), 15 | banner = 16 | "// svg-pan-zoom v<%= pkg.version %>" + 17 | "\n" + 18 | "// https://github.com/bumbu/svg-pan-zoom" + 19 | "\n"; 20 | 21 | function isFixed(file) { 22 | // TODO: why is file.eslint undefined? 23 | return typeof file.eslint === "object" && file.eslint.fixed; 24 | } 25 | 26 | /** 27 | * Build script 28 | */ 29 | function compile() { 30 | return browserify({ entries: "./src/stand-alone.js" }) 31 | .bundle() 32 | .on("error", function(err) { 33 | console.log(err.toString()); 34 | this.emit("end"); 35 | }) 36 | .pipe(source("svg-pan-zoom.js")) 37 | .pipe(buffer()) 38 | .pipe(header(banner, { pkg: pkg })) 39 | .pipe(gulp.dest("./dist/")) 40 | .pipe(rename("svg-pan-zoom.min.js")) 41 | .pipe(uglify()) 42 | .pipe(header(banner, { pkg: pkg })) 43 | .pipe(gulp.dest("./dist/")); 44 | } 45 | 46 | /** 47 | * Watch script 48 | */ 49 | function watch() { 50 | return gulp.watch("./src/**/*.js", gulp.series("compile")); 51 | } 52 | 53 | /** 54 | * Test task 55 | */ 56 | function test() { 57 | return gulp.src("./tests/index.html").pipe(qunit()); 58 | } 59 | 60 | /** 61 | * Check 62 | */ 63 | function check() { 64 | return ( 65 | gulp 66 | .src([ 67 | "./**/*.js", 68 | "!./dist/**/*.js", 69 | "!./demo/**/*.js", 70 | "!./tests/assets/**/*.js", 71 | "!./src/uniwheel.js" // Ignore uniwheel 72 | ]) 73 | // NOTE: this runs prettier via eslint-plugin-prettier 74 | .pipe( 75 | eslint({ 76 | configFile: "./.eslintrc.json", 77 | fix: true 78 | }) 79 | ) 80 | .pipe(eslint.format()) 81 | .pipe(gulpIf(isFixed, gulp.dest("./"))) 82 | // uncomment to stop on error 83 | .pipe(eslint.failAfterError()) 84 | ); 85 | } 86 | 87 | exports.compile = compile; 88 | exports.watch = watch; 89 | exports.test = test; 90 | exports.check = check; 91 | 92 | /** 93 | * Build 94 | */ 95 | exports.build = gulp.series([check, compile, test]); 96 | 97 | /** 98 | * Default task 99 | */ 100 | exports.default = gulp.series([compile, watch]); 101 | -------------------------------------------------------------------------------- /demo/limit-pan.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /src/media/reset.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 11 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /demo/simple-animation.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |

Demo for svg-pan-zoom: In-line SVG

10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /tests/test_typescript.ts: -------------------------------------------------------------------------------- 1 | var svgPanZoomOptions : SvgPanZoom.Options = { 2 | panEnabled: true // enable or disable panning (default enabled) 3 | , controlIconsEnabled: false // insert icons to give user an option in addition to mouse events to control pan/zoom (default disabled) 4 | , zoomEnabled: true // enable or disable zooming (default enabled) 5 | , dblClickZoomEnabled: true // enable or disable zooming by double clicking (default enabled) 6 | , zoomScaleSensitivity: 0.2 // Zoom sensitivity 7 | , minZoom: 0.5 // Minimum Zoom level 8 | , maxZoom: 10 // Maximum Zoom level 9 | , fit: true // enable or disable viewport fit in SVG (default true) 10 | , center: true // enable or disable viewport centering in SVG (default true) 11 | , beforeZoom: null 12 | , onZoom: function(){} 13 | , beforePan: null 14 | , onPan: function(){} 15 | , refreshRate: 60 // in hz 16 | }; 17 | 18 | var panZoomTiger: SvgPanZoom.Instance = svgPanZoom('#demo-tiger'); 19 | 20 | var svgElement = document.querySelector('#demo-tiger'); 21 | panZoomTiger = svgPanZoom(svgElement); 22 | 23 | panZoomTiger = svgPanZoom('#demo-tiger', { 24 | panEnabled: true 25 | , controlIconsEnabled: false 26 | , zoomEnabled: true 27 | , dblClickZoomEnabled: true 28 | , mouseWheelZoomEnabled: true 29 | , preventMouseEventsDefault: true 30 | , zoomScaleSensitivity: 0.2 31 | , minZoom: 0.5 32 | , maxZoom: 10 33 | , fit: true 34 | , contain: false 35 | , center: true 36 | , refreshRate: 'auto' 37 | , beforeZoom: function(oldScale, newScale){ return true; } 38 | , onZoom: function(newScale){} 39 | , beforePan: function(oldPan, newPan){ return {x: true, y: true}; } 40 | , customEventsHandler: null 41 | , eventsListenerElement: null 42 | }); 43 | 44 | /** 45 | * PAN 46 | */ 47 | // Pan to rendered point x = 50, y = 50 48 | panZoomTiger.pan({x: 50, y: 50}); 49 | 50 | // Pan by x = 50, y = 50 of rendered pixels 51 | panZoomTiger.panBy({x: 50, y: 50}); 52 | 53 | panZoomTiger.getPan(); 54 | 55 | panZoomTiger.resetPan(); 56 | 57 | panZoomTiger.enablePan(); 58 | panZoomTiger.disablePan(); 59 | 60 | panZoomTiger.isPanEnabled(); 61 | 62 | /** 63 | * ZOOM 64 | */ 65 | // Set zoom level to 2 66 | panZoomTiger.zoom(2); 67 | 68 | // Zoom by 130% 69 | panZoomTiger.zoomBy(1.3); 70 | 71 | // Set zoom level to 2 at point 72 | panZoomTiger.zoomAtPoint(2, {x: 50, y: 50}); 73 | 74 | // Zoom by 130% at point 75 | panZoomTiger.zoomAtPointBy(1.3, {x: 50, y: 50}); 76 | 77 | panZoomTiger.zoomIn(); 78 | panZoomTiger.zoomOut(); 79 | 80 | panZoomTiger.getZoom(); 81 | 82 | panZoomTiger.resetZoom(); 83 | 84 | panZoomTiger.enableZoom(); 85 | panZoomTiger.disableZoom(); 86 | 87 | panZoomTiger.isZoomEnabled(); 88 | 89 | /** 90 | * controls 91 | */ 92 | panZoomTiger.enableControlIcons(); 93 | panZoomTiger.disableControlIcons(); 94 | panZoomTiger.isControlIconsEnabled(); 95 | 96 | panZoomTiger.enableDblClickZoom(); 97 | panZoomTiger.disableDblClickZoom(); 98 | panZoomTiger.isDblClickZoomEnabled(); 99 | 100 | panZoomTiger.enableMouseWheelZoom(); 101 | panZoomTiger.disableMouseWheelZoom(); 102 | panZoomTiger.isMouseWheelZoomEnabled(); 103 | 104 | panZoomTiger.center(); 105 | panZoomTiger.fit(); 106 | panZoomTiger.contain(); 107 | 108 | panZoomTiger.resize(); // update SVG cached size and controls positions 109 | panZoomTiger.getSizes(); 110 | 111 | panZoomTiger.updateBBox(); 112 | 113 | panZoomTiger.fit().center().zoomBy(5); // can chain method 114 | 115 | panZoomTiger.destroy(); // destroy instance 116 | 117 | panZoomTiger = null; 118 | -------------------------------------------------------------------------------- /demo/custom-event-handlers.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 17 | 18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 |
36 | 37 | 97 | 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /demo/mobile.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 98 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /src/uniwheel.js: -------------------------------------------------------------------------------- 1 | // uniwheel 0.1.2 (customized) 2 | // A unified cross browser mouse wheel event handler 3 | // https://github.com/teemualap/uniwheel 4 | 5 | module.exports = (function(){ 6 | 7 | //Full details: https://developer.mozilla.org/en-US/docs/Web/Reference/Events/wheel 8 | 9 | var prefix = "", _addEventListener, _removeEventListener, support, fns = []; 10 | var passiveListenerOption = {passive: true}; 11 | var activeListenerOption = {passive: false}; 12 | 13 | // detect event model 14 | if ( window.addEventListener ) { 15 | _addEventListener = "addEventListener"; 16 | _removeEventListener = "removeEventListener"; 17 | } else { 18 | _addEventListener = "attachEvent"; 19 | _removeEventListener = "detachEvent"; 20 | prefix = "on"; 21 | } 22 | 23 | // detect available wheel event 24 | support = "onwheel" in document.createElement("div") ? "wheel" : // Modern browsers support "wheel" 25 | document.onmousewheel !== undefined ? "mousewheel" : // Webkit and IE support at least "mousewheel" 26 | "DOMMouseScroll"; // let's assume that remaining browsers are older Firefox 27 | 28 | 29 | function createCallback(element,callback) { 30 | 31 | var fn = function(originalEvent) { 32 | 33 | !originalEvent && ( originalEvent = window.event ); 34 | 35 | // create a normalized event object 36 | var event = { 37 | // keep a ref to the original event object 38 | originalEvent: originalEvent, 39 | target: originalEvent.target || originalEvent.srcElement, 40 | type: "wheel", 41 | deltaMode: originalEvent.type == "MozMousePixelScroll" ? 0 : 1, 42 | deltaX: 0, 43 | delatZ: 0, 44 | preventDefault: function() { 45 | originalEvent.preventDefault ? 46 | originalEvent.preventDefault() : 47 | originalEvent.returnValue = false; 48 | } 49 | }; 50 | 51 | // calculate deltaY (and deltaX) according to the event 52 | if ( support == "mousewheel" ) { 53 | event.deltaY = - 1/40 * originalEvent.wheelDelta; 54 | // Webkit also support wheelDeltaX 55 | originalEvent.wheelDeltaX && ( event.deltaX = - 1/40 * originalEvent.wheelDeltaX ); 56 | } else { 57 | event.deltaY = originalEvent.detail; 58 | } 59 | 60 | // it's time to fire the callback 61 | return callback( event ); 62 | 63 | }; 64 | 65 | fns.push({ 66 | element: element, 67 | fn: fn, 68 | }); 69 | 70 | return fn; 71 | } 72 | 73 | function getCallback(element) { 74 | for (var i = 0; i < fns.length; i++) { 75 | if (fns[i].element === element) { 76 | return fns[i].fn; 77 | } 78 | } 79 | return function(){}; 80 | } 81 | 82 | function removeCallback(element) { 83 | for (var i = 0; i < fns.length; i++) { 84 | if (fns[i].element === element) { 85 | return fns.splice(i,1); 86 | } 87 | } 88 | } 89 | 90 | function _addWheelListener(elem, eventName, callback, isPassiveListener ) { 91 | var cb; 92 | 93 | if (support === "wheel") { 94 | cb = callback; 95 | } else { 96 | cb = createCallback(elem, callback); 97 | } 98 | 99 | elem[_addEventListener]( 100 | prefix + eventName, 101 | cb, 102 | isPassiveListener ? passiveListenerOption : activeListenerOption 103 | ); 104 | } 105 | 106 | function _removeWheelListener(elem, eventName, callback, isPassiveListener ) { 107 | 108 | var cb; 109 | 110 | if (support === "wheel") { 111 | cb = callback; 112 | } else { 113 | cb = getCallback(elem); 114 | } 115 | 116 | elem[_removeEventListener]( 117 | prefix + eventName, 118 | cb, 119 | isPassiveListener ? passiveListenerOption : activeListenerOption 120 | ); 121 | 122 | removeCallback(elem); 123 | } 124 | 125 | function addWheelListener( elem, callback, isPassiveListener ) { 126 | _addWheelListener(elem, support, callback, isPassiveListener ); 127 | 128 | // handle MozMousePixelScroll in older Firefox 129 | if( support == "DOMMouseScroll" ) { 130 | _addWheelListener(elem, "MozMousePixelScroll", callback, isPassiveListener ); 131 | } 132 | } 133 | 134 | function removeWheelListener(elem, callback, isPassiveListener){ 135 | _removeWheelListener(elem, support, callback, isPassiveListener); 136 | 137 | // handle MozMousePixelScroll in older Firefox 138 | if( support == "DOMMouseScroll" ) { 139 | _removeWheelListener(elem, "MozMousePixelScroll", callback, isPassiveListener); 140 | } 141 | } 142 | 143 | return { 144 | on: addWheelListener, 145 | off: removeWheelListener 146 | }; 147 | 148 | })(); 149 | -------------------------------------------------------------------------------- /tests/assets/qunit.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * QUnit 1.15.0 3 | * http://qunitjs.com/ 4 | * 5 | * Copyright 2014 jQuery Foundation and other contributors 6 | * Released under the MIT license 7 | * http://jquery.org/license 8 | * 9 | * Date: 2014-08-08T16:00Z 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 1em 0.5em 1em; 66 | color: #5E740B; 67 | background-color: #EEE; 68 | overflow: hidden; 69 | } 70 | 71 | #qunit-userAgent { 72 | padding: 0.5em 1em 0.5em 1em; 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 1em 0.4em 1em; 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 1em 0.5em 1em; 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 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var util = require("util"), 4 | http = require("http"), 5 | fs = require("fs"), 6 | url = require("url"), 7 | events = require("events"); 8 | 9 | var DEFAULT_PORT = 3001; 10 | 11 | function main(argv) { 12 | new HttpServer({ 13 | GET: createServlet(StaticServlet), 14 | HEAD: createServlet(StaticServlet) 15 | }).start(Number(argv[2]) || DEFAULT_PORT); 16 | } 17 | 18 | function escapeHtml(value) { 19 | return value 20 | .toString() 21 | .replace("<", "<") 22 | .replace(">", ">") 23 | .replace('"', """); 24 | } 25 | 26 | function createServlet(Class) { 27 | var servlet = new Class(); 28 | return servlet.handleRequest.bind(servlet); 29 | } 30 | 31 | /** 32 | * An Http server implementation that uses a map of methods to decide 33 | * action routing. 34 | * 35 | * @param {Object} Map of method => Handler function 36 | */ 37 | function HttpServer(handlers) { 38 | this.handlers = handlers; 39 | this.server = http.createServer(this.handleRequest_.bind(this)); 40 | } 41 | 42 | HttpServer.prototype.start = function(port) { 43 | this.port = port; 44 | this.server.listen(port); 45 | util.puts("Http Server running at http://localhost:" + port + "/"); 46 | }; 47 | 48 | HttpServer.prototype.parseUrl_ = function(urlString) { 49 | var parsed = url.parse(urlString); 50 | parsed.pathname = url.resolve("/", parsed.pathname); 51 | return url.parse(url.format(parsed), true); 52 | }; 53 | 54 | HttpServer.prototype.handleRequest_ = function(req, res) { 55 | var logEntry = req.method + " " + req.url; 56 | if (req.headers["user-agent"]) { 57 | logEntry += " " + req.headers["user-agent"]; 58 | } 59 | util.puts(logEntry); 60 | req.url = this.parseUrl_(req.url); 61 | var handler = this.handlers[req.method]; 62 | if (!handler) { 63 | res.writeHead(501); 64 | res.end(); 65 | } else { 66 | handler.call(this, req, res); 67 | } 68 | }; 69 | 70 | /** 71 | * Handles static content. 72 | */ 73 | function StaticServlet() {} 74 | 75 | StaticServlet.MimeMap = { 76 | txt: "text/plain", 77 | html: "text/html", 78 | css: "text/css", 79 | xml: "application/xml", 80 | json: "application/json", 81 | js: "application/javascript", 82 | jpg: "image/jpeg", 83 | jpeg: "image/jpeg", 84 | gif: "image/gif", 85 | png: "image/png", 86 | svg: "image/svg+xml" 87 | }; 88 | 89 | StaticServlet.prototype.handleRequest = function(req, res) { 90 | var that = this; 91 | var path = ("./" + req.url.pathname) 92 | .replace("//", "/") 93 | .replace(/%(..)/g, function(match, hex) { 94 | return String.fromCharCode(parseInt(hex, 16)); 95 | }); 96 | var parts = path.split("/"); 97 | if (parts[parts.length - 1].charAt(0) === ".") 98 | return that.sendForbidden_(req, res, path); 99 | fs.stat(path, function(err, stat) { 100 | if (err) return that.sendMissing_(req, res, path); 101 | if (stat.isDirectory()) return that.sendDirectory_(req, res, path); 102 | return that.sendFile_(req, res, path); 103 | }); 104 | }; 105 | 106 | StaticServlet.prototype.sendError_ = function(req, res, error) { 107 | res.writeHead(500, { 108 | "Content-Type": "text/html" 109 | }); 110 | res.write("\n"); 111 | res.write("Internal Server Error\n"); 112 | res.write("

Internal Server Error

"); 113 | res.write("
" + escapeHtml(util.inspect(error)) + "
"); 114 | util.puts("500 Internal Server Error"); 115 | util.puts(util.inspect(error)); 116 | }; 117 | 118 | StaticServlet.prototype.sendMissing_ = function(req, res, path) { 119 | path = path.substring(1); 120 | res.writeHead(404, { 121 | "Content-Type": "text/html" 122 | }); 123 | res.write("\n"); 124 | res.write("404 Not Found\n"); 125 | res.write("

Not Found

"); 126 | res.write( 127 | "

The requested URL " + 128 | escapeHtml(path) + 129 | " was not found on this server.

" 130 | ); 131 | res.end(); 132 | util.puts("404 Not Found: " + path); 133 | }; 134 | 135 | StaticServlet.prototype.sendForbidden_ = function(req, res, path) { 136 | path = path.substring(1); 137 | res.writeHead(403, { 138 | "Content-Type": "text/html" 139 | }); 140 | res.write("\n"); 141 | res.write("403 Forbidden\n"); 142 | res.write("

Forbidden

"); 143 | res.write( 144 | "

You do not have permission to access " + 145 | escapeHtml(path) + 146 | " on this server.

" 147 | ); 148 | res.end(); 149 | util.puts("403 Forbidden: " + path); 150 | }; 151 | 152 | StaticServlet.prototype.sendRedirect_ = function(req, res, redirectUrl) { 153 | res.writeHead(301, { 154 | "Content-Type": "text/html", 155 | Location: redirectUrl 156 | }); 157 | res.write("\n"); 158 | res.write("301 Moved Permanently\n"); 159 | res.write("

Moved Permanently

"); 160 | res.write( 161 | '

The document has moved here.

' 162 | ); 163 | res.end(); 164 | util.puts("301 Moved Permanently: " + redirectUrl); 165 | }; 166 | 167 | StaticServlet.prototype.sendFile_ = function(req, res, path) { 168 | var that = this; 169 | var file = fs.createReadStream(path); 170 | res.writeHead(200, { 171 | "Content-Type": StaticServlet.MimeMap[path.split(".").pop()] || "text/plain" 172 | }); 173 | if (req.method === "HEAD") { 174 | res.end(); 175 | } else { 176 | file.on("data", res.write.bind(res)); 177 | file.on("close", function() { 178 | res.end(); 179 | }); 180 | file.on("error", function(error) { 181 | that.sendError_(req, res, error); 182 | }); 183 | } 184 | }; 185 | 186 | StaticServlet.prototype.sendDirectory_ = function(req, res, path) { 187 | var that = this; 188 | if (path.match(/[^\/]$/)) { 189 | req.url.pathname += "/"; 190 | var redirectUrl = url.format(url.parse(url.format(req.url))); 191 | return that.sendRedirect_(req, res, redirectUrl); 192 | } 193 | fs.readdir(path, function(err, files) { 194 | if (err) return that.sendError_(req, res, error); 195 | 196 | if (!files.length) return that.writeDirectoryIndex_(req, res, path, []); 197 | 198 | var remaining = files.length; 199 | files.forEach(function(fileName, index) { 200 | fs.stat(path + "/" + fileName, function(err, stat) { 201 | if (err) return that.sendError_(req, res, err); 202 | if (stat.isDirectory()) { 203 | files[index] = fileName + "/"; 204 | } 205 | if (!--remaining) 206 | return that.writeDirectoryIndex_(req, res, path, files); 207 | }); 208 | }); 209 | }); 210 | }; 211 | 212 | StaticServlet.prototype.writeDirectoryIndex_ = function(req, res, path, files) { 213 | path = path.substring(1); 214 | res.writeHead(200, { 215 | "Content-Type": "text/html" 216 | }); 217 | if (req.method === "HEAD") { 218 | res.end(); 219 | return; 220 | } 221 | res.write("\n"); 222 | res.write("" + escapeHtml(path) + "\n"); 223 | res.write("\n"); 226 | res.write("

Directory: " + escapeHtml(path) + "

"); 227 | res.write("
    "); 228 | files.forEach(function(fileName) { 229 | if (fileName.charAt(0) !== ".") { 230 | res.write( 231 | '
  1. ' + 234 | escapeHtml(fileName) + 235 | "
  2. " 236 | ); 237 | } 238 | }); 239 | res.write("
"); 240 | res.end(); 241 | }; 242 | 243 | // Must be last, 244 | main(process.argv); 245 | -------------------------------------------------------------------------------- /demo/thumbnailViewer.js: -------------------------------------------------------------------------------- 1 | var thumbnailViewer = function(options){ 2 | 3 | var getSVGDocument = function(objectElem){ 4 | var svgDoc = objectElem.contentDocument; 5 | if(! svgDoc){ 6 | svgDoc = objectElem.getSVGDocument(); 7 | } 8 | return svgDoc; 9 | } 10 | 11 | var bindThumbnail = function(main, thumb){ 12 | 13 | if(! window.main && main){ 14 | window.main = main; 15 | } 16 | if(! window.thumb && thumb){ 17 | window.thumb = thumb; 18 | } 19 | if(! window.main || ! window.thumb){ 20 | return; 21 | } 22 | 23 | var resizeTimer; 24 | var interval = 300; //msec 25 | window.addEventListener('resize', function(event){ 26 | if (resizeTimer !== false) { 27 | clearTimeout(resizeTimer); 28 | } 29 | resizeTimer = setTimeout(function () { 30 | window.main.resize(); 31 | window.thumb.resize(); 32 | }, interval); 33 | }); 34 | 35 | window.main.setOnZoom(function(level){ 36 | window.thumb.updateThumbScope(); 37 | if(options.onZoom){ 38 | options.onZoom(window.main, window.thumb, level); 39 | } 40 | }); 41 | 42 | window.main.setOnPan(function(point){ 43 | window.thumb.updateThumbScope(); 44 | if(options.onPan){ 45 | options.onPan(window.main, window.thumb, point); 46 | } 47 | }); 48 | 49 | var _updateThumbScope = function (main, thumb, scope, line1, line2){ 50 | var mainPanX = main.getPan().x 51 | , mainPanY = main.getPan().y 52 | , mainWidth = main.getSizes().width 53 | , mainHeight = main.getSizes().height 54 | , mainZoom = main.getSizes().realZoom 55 | , thumbPanX = thumb.getPan().x 56 | , thumbPanY = thumb.getPan().y 57 | , thumbZoom = thumb.getSizes().realZoom; 58 | 59 | var thumByMainZoomRatio = thumbZoom / mainZoom; 60 | 61 | var scopeX = thumbPanX - mainPanX * thumByMainZoomRatio; 62 | var scopeY = thumbPanY - mainPanY * thumByMainZoomRatio; 63 | var scopeWidth = mainWidth * thumByMainZoomRatio; 64 | var scopeHeight = mainHeight * thumByMainZoomRatio; 65 | 66 | scope.setAttribute("x", scopeX + 1); 67 | scope.setAttribute("y", scopeY + 1); 68 | scope.setAttribute("width", scopeWidth - 2); 69 | scope.setAttribute("height", scopeHeight - 2); 70 | /* 71 | line1.setAttribute("x1", scopeX + 1); 72 | line1.setAttribute("y1", scopeY + 1); 73 | line1.setAttribute("x2", scopeX + 1 + scopeWidth - 2); 74 | line1.setAttribute("y2", scopeY + 1 + scopeHeight - 2); 75 | line2.setAttribute("x1", scopeX + 1); 76 | line2.setAttribute("y1", scopeY + 1 + scopeHeight - 2); 77 | line2.setAttribute("x2", scopeX + 1 + scopeWidth - 2); 78 | line2.setAttribute("y2", scopeY + 1); 79 | */ 80 | }; 81 | 82 | window.thumb.updateThumbScope = function(){ 83 | var scope = document.getElementById('scope'); 84 | var line1 = document.getElementById('line1'); 85 | var line2 = document.getElementById('line2'); 86 | _updateThumbScope(window.main, window.thumb, scope, line1, line2); 87 | } 88 | window.thumb.updateThumbScope(); 89 | 90 | var _updateMainViewPan = function(clientX, clientY, scopeContainer, main, thumb){ 91 | var dim = scopeContainer.getBoundingClientRect() 92 | , mainWidth = main.getSizes().width 93 | , mainHeight = main.getSizes().height 94 | , mainZoom = main.getSizes().realZoom 95 | , thumbWidth = thumb.getSizes().width 96 | , thumbHeight = thumb.getSizes().height 97 | , thumbZoom = thumb.getSizes().realZoom; 98 | 99 | var thumbPanX = clientX - dim.left - thumbWidth / 2; 100 | var thumbPanY = clientY - dim.top - thumbHeight / 2; 101 | var mainPanX = - thumbPanX * mainZoom / thumbZoom; 102 | var mainPanY = - thumbPanY * mainZoom / thumbZoom; 103 | main.pan({x:mainPanX, y:mainPanY}); 104 | }; 105 | 106 | var updateMainViewPan = function(evt){ 107 | if(evt.which == 0 && evt.button == 0){ 108 | return false; 109 | } 110 | var scopeContainer = document.getElementById('scopeContainer'); 111 | _updateMainViewPan(evt.clientX, evt.clientY, scopeContainer, window.main, window.thumb); 112 | } 113 | 114 | var scopeContainer = document.getElementById('scopeContainer'); 115 | scopeContainer.addEventListener('click', function(evt){ 116 | updateMainViewPan(evt); 117 | }); 118 | 119 | scopeContainer.addEventListener('mousemove', function(evt){ 120 | updateMainViewPan(evt); 121 | }); 122 | }; 123 | 124 | var mainViewObjectElem = document.getElementById(options.mainViewId); 125 | mainViewObjectElem.addEventListener("load", function(){ 126 | 127 | var mainViewSVGDoc = getSVGDocument(mainViewObjectElem); 128 | if(options.onMainViewSVGLoaded){ 129 | options.onMainViewSVGLoaded(mainViewSVGDoc); 130 | } 131 | 132 | var beforePan = function(oldPan, newPan){ 133 | var stopHorizontal = false 134 | , stopVertical = false 135 | , gutterWidth = 100 136 | , gutterHeight = 100 137 | // Computed variables 138 | , sizes = this.getSizes() 139 | , leftLimit = -((sizes.viewBox.x + sizes.viewBox.width) * sizes.realZoom) + gutterWidth 140 | , rightLimit = sizes.width - gutterWidth - (sizes.viewBox.x * sizes.realZoom) 141 | , topLimit = -((sizes.viewBox.y + sizes.viewBox.height) * sizes.realZoom) + gutterHeight 142 | , bottomLimit = sizes.height - gutterHeight - (sizes.viewBox.y * sizes.realZoom); 143 | customPan = {}; 144 | customPan.x = Math.max(leftLimit, Math.min(rightLimit, newPan.x)); 145 | customPan.y = Math.max(topLimit, Math.min(bottomLimit, newPan.y)); 146 | return customPan; 147 | }; 148 | 149 | var main = svgPanZoom('#'+options.mainViewId, { 150 | zoomEnabled: true, 151 | controlIconsEnabled: true, 152 | fit: true, 153 | center: true, 154 | beforePan: beforePan 155 | }); 156 | 157 | bindThumbnail(main, undefined); 158 | if(options.onMainViewShown){ 159 | options.onMainViewShown(mainViewSVGDoc, main); 160 | } 161 | 162 | }, false); 163 | 164 | var thumbViewObjectElem = document.getElementById(options.thumbViewId); 165 | thumbViewObjectElem.addEventListener("load", function(){ 166 | 167 | var thumbViewSVGDoc = getSVGDocument(thumbViewObjectElem); 168 | if(options.onThumbnailSVGLoaded){ 169 | options.onThumbnailSVGLoaded(thumbViewSVGDoc); 170 | } 171 | 172 | var thumb = svgPanZoom('#'+options.thumbViewId, { 173 | zoomEnabled: false, 174 | panEnabled: false, 175 | controlIconsEnabled: false, 176 | dblClickZoomEnabled: false, 177 | preventMouseEventsDefault: true, 178 | }); 179 | 180 | bindThumbnail(undefined, thumb); 181 | if(options.onThumbnailShown){ 182 | options.onThumbnailShown(thumbViewSVGDoc, thumb); 183 | } 184 | 185 | }, false); 186 | }; 187 | 188 | -------------------------------------------------------------------------------- /dist/svg-pan-zoom.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for svg-pan-zoom v3.5.0 2 | // Project: https://github.com/bumbu/svg-pan-zoom 3 | // Definitions by: César Vidril 4 | // Definitions: https://github.com/bumbu/svg-pan-zoom 5 | 6 | declare namespace SvgPanZoom { 7 | interface Options { 8 | /** 9 | * can be querySelector string or SVGElement (default enabled) 10 | * @type {string|HTMLElement|SVGElement} 11 | */ 12 | viewportSelector?: string|HTMLElement|SVGElement; 13 | /** 14 | * enable or disable panning (default enabled) 15 | * @type {boolean} 16 | */ 17 | panEnabled?: boolean; 18 | /** 19 | * insert icons to give user an option in addition to mouse events to control pan/zoom (default disabled) 20 | * @type {boolean} 21 | */ 22 | controlIconsEnabled?: boolean; 23 | /** 24 | * enable or disable zooming (default enabled) 25 | * @type {boolean} 26 | */ 27 | zoomEnabled?: boolean; 28 | /** 29 | * enable or disable zooming by double clicking (default enabled) 30 | * @type {boolean} 31 | */ 32 | dblClickZoomEnabled?: boolean; 33 | /** 34 | * enable or disable zooming by scrolling (default enabled) 35 | * @type {boolean} 36 | */ 37 | mouseWheelZoomEnabled?: boolean; 38 | /** 39 | * prevent mouse events to bubble up (default enabled) 40 | * @type {boolean} 41 | */ 42 | preventMouseEventsDefault?: boolean; 43 | zoomScaleSensitivity?: number; // Zoom sensitivity (Default 0.2) 44 | minZoom?: number; // Minimum Zoom level (Default 0.5) 45 | maxZoom?: number; // Maximum Zoom level (Default 10) 46 | fit?: boolean; // enable or disable viewport fit in SVG (default true) 47 | contain?: boolean; // (default true) 48 | center?: boolean; // enable or disable viewport centering in SVG (default true) 49 | refreshRate?: number | "auto"; // (default 'auto') 50 | beforeZoom?: (oldScale: number, newScale: number) => void | boolean; 51 | onZoom?: (newScale: number) => void; 52 | beforePan?: (oldPan: Point, newPan: Point) => void | boolean | PointModifier; 53 | onPan?: (newPan: Point) => void; 54 | onUpdatedCTM?: (newCTM: SVGMatrix) => void; 55 | customEventsHandler?: CustomEventHandler; // (default null) 56 | eventsListenerElement?: SVGElement; // (default null) 57 | } 58 | 59 | interface CustomEventHandler { 60 | init: (options: CustomEventOptions) => void; 61 | haltEventListeners: string[]; 62 | destroy: Function; 63 | } 64 | 65 | interface CustomEventOptions { 66 | svgElement: SVGSVGElement; 67 | instance: Instance; 68 | } 69 | 70 | interface Point { 71 | x: number; 72 | y: number; 73 | } 74 | 75 | interface PointModifier { 76 | x: number|boolean; 77 | y: number|boolean; 78 | } 79 | 80 | interface Sizes { 81 | width: number; 82 | height: number; 83 | realZoom: number; 84 | viewBox: { 85 | x: number; 86 | y: number; 87 | width: number; 88 | height: number; 89 | }; 90 | } 91 | 92 | interface Instance { 93 | /** 94 | * Creates a new SvgPanZoom instance with given element selector. 95 | * 96 | * @param {string|HTMLElement|SVGElement} svg selector of the tag on which it is to be applied. 97 | * @param {Object} options provides customization options at the initialization of the object. 98 | * @return {Instance} Current instance 99 | */ 100 | (svg: string|HTMLElement|SVGElement, options?: Options): Instance; 101 | 102 | /** 103 | * Enables Panning on svg element 104 | * @return {Instance} Current instance 105 | */ 106 | enablePan(): Instance; 107 | 108 | /** 109 | * Disables panning on svg element 110 | * @return {Instance} Current instance 111 | */ 112 | disablePan(): Instance; 113 | 114 | /** 115 | * Checks if Panning is enabled or not 116 | * @return {Boolean} true or false based on panning settings 117 | */ 118 | isPanEnabled(): boolean; 119 | 120 | setBeforePan(fn: (oldPoint: Point, newPoint: Point) => void | boolean | PointModifier): Instance; 121 | 122 | setOnPan(fn: (point: Point) => void): Instance; 123 | 124 | /** 125 | * Pan to a rendered position 126 | * 127 | * @param {Object} point {x: 0, y: 0} 128 | * @return {Instance} Current instance 129 | */ 130 | pan(point: Point): Instance; 131 | 132 | /** 133 | * Relatively pan the graph by a specified rendered position vector 134 | * 135 | * @param {Object} point {x: 0, y: 0} 136 | * @return {Instance} Current instance 137 | */ 138 | panBy(point: Point): Instance; 139 | 140 | /** 141 | * Get pan vector 142 | * 143 | * @return {Object} {x: 0, y: 0} 144 | * @return {Instance} Current instance 145 | */ 146 | getPan(): Point; 147 | 148 | resetPan(): Instance; 149 | 150 | enableZoom(): Instance; 151 | 152 | disableZoom(): Instance; 153 | 154 | isZoomEnabled(): boolean; 155 | 156 | enableControlIcons(): Instance; 157 | 158 | disableControlIcons(): Instance; 159 | 160 | isControlIconsEnabled(): boolean; 161 | 162 | enableDblClickZoom(): Instance; 163 | 164 | disableDblClickZoom(): Instance; 165 | 166 | isDblClickZoomEnabled(): boolean; 167 | 168 | enableMouseWheelZoom(): Instance; 169 | 170 | disableMouseWheelZoom(): Instance; 171 | 172 | isMouseWheelZoomEnabled(): boolean; 173 | 174 | setZoomScaleSensitivity(scale: number): Instance; 175 | 176 | setMinZoom(zoom: number): Instance; 177 | 178 | setMaxZoom(zoom: number): Instance; 179 | 180 | setBeforeZoom(fn: (oldScale: number, newScale: number) => void | boolean): Instance; 181 | 182 | setOnZoom(fn: (scale: number) => void): Instance; 183 | 184 | zoom(scale: number): void; 185 | 186 | zoomIn(): Instance; 187 | 188 | zoomOut(): Instance; 189 | 190 | zoomBy(scale: number): Instance; 191 | 192 | zoomAtPoint(scale: number, point: Point): Instance; 193 | 194 | zoomAtPointBy(scale: number, point: Point): Instance; 195 | 196 | resetZoom(): Instance; 197 | 198 | /** 199 | * Get zoom scale/level 200 | * 201 | * @return {float} zoom scale 202 | */ 203 | getZoom(): number; 204 | 205 | setOnUpdatedCTM(fn: (newCTM: SVGMatrix) => void): Instance; 206 | 207 | /** 208 | * Adjust viewport size (only) so it will fit in SVG 209 | * Does not center image 210 | * 211 | * @return {Instance} Current instance 212 | */ 213 | fit(): Instance; 214 | 215 | /** 216 | * Adjust viewport size (only) so it will contain in SVG 217 | * Does not center image 218 | * 219 | * @return {Instance} Current instance 220 | */ 221 | contain(): Instance; 222 | 223 | /** 224 | * Adjust viewport pan (only) so it will be centered in SVG 225 | * Does not zoom/fit image 226 | * 227 | * @return {Instance} Current instance 228 | */ 229 | center(): Instance; 230 | 231 | /** 232 | * Recalculates cached svg dimensions and controls position 233 | * 234 | * @return {Instance} Current instance 235 | */ 236 | resize(): Instance; 237 | 238 | /** 239 | * Get all calculate svg dimensions 240 | * 241 | * @return {Object} {width: 0, height: 0, realZoom: 0, viewBox: { width: 0, height: 0 }} 242 | */ 243 | getSizes(): Sizes; 244 | 245 | reset(): Instance; 246 | 247 | /** 248 | * Update content cached BorderBox 249 | * Use when viewport contents change 250 | * 251 | * @return {Instance} Current instance 252 | */ 253 | updateBBox(): Instance; 254 | 255 | destroy(): void; 256 | } 257 | } 258 | 259 | declare const svgPanZoom: SvgPanZoom.Instance; 260 | 261 | declare module "svg-pan-zoom" { 262 | export = svgPanZoom; 263 | } 264 | -------------------------------------------------------------------------------- /src/svg-utilities.js: -------------------------------------------------------------------------------- 1 | var Utils = require("./utilities"), 2 | _browser = "unknown"; 3 | 4 | // http://stackoverflow.com/questions/9847580/how-to-detect-safari-chrome-ie-firefox-and-opera-browser 5 | if (/*@cc_on!@*/ false || !!document.documentMode) { 6 | // internet explorer 7 | _browser = "ie"; 8 | } 9 | 10 | module.exports = { 11 | svgNS: "http://www.w3.org/2000/svg", 12 | xmlNS: "http://www.w3.org/XML/1998/namespace", 13 | xmlnsNS: "http://www.w3.org/2000/xmlns/", 14 | xlinkNS: "http://www.w3.org/1999/xlink", 15 | evNS: "http://www.w3.org/2001/xml-events", 16 | 17 | /** 18 | * Get svg dimensions: width and height 19 | * 20 | * @param {SVGSVGElement} svg 21 | * @return {Object} {width: 0, height: 0} 22 | */ 23 | getBoundingClientRectNormalized: function(svg) { 24 | if (svg.clientWidth && svg.clientHeight) { 25 | return { width: svg.clientWidth, height: svg.clientHeight }; 26 | } else if (!!svg.getBoundingClientRect()) { 27 | return svg.getBoundingClientRect(); 28 | } else { 29 | throw new Error("Cannot get BoundingClientRect for SVG."); 30 | } 31 | }, 32 | 33 | /** 34 | * Gets g element with class of "viewport" or creates it if it doesn't exist 35 | * 36 | * @param {SVGSVGElement} svg 37 | * @return {SVGElement} g (group) element 38 | */ 39 | getOrCreateViewport: function(svg, selector) { 40 | var viewport = null; 41 | 42 | if (Utils.isElement(selector)) { 43 | viewport = selector; 44 | } else { 45 | viewport = svg.querySelector(selector); 46 | } 47 | 48 | // Check if there is just one main group in SVG 49 | if (!viewport) { 50 | var childNodes = Array.prototype.slice 51 | .call(svg.childNodes || svg.children) 52 | .filter(function(el) { 53 | return el.nodeName !== "defs" && el.nodeName !== "#text"; 54 | }); 55 | 56 | // Node name should be SVGGElement and should have no transform attribute 57 | // Groups with transform are not used as viewport because it involves parsing of all transform possibilities 58 | if ( 59 | childNodes.length === 1 && 60 | childNodes[0].nodeName === "g" && 61 | childNodes[0].getAttribute("transform") === null 62 | ) { 63 | viewport = childNodes[0]; 64 | } 65 | } 66 | 67 | // If no favorable group element exists then create one 68 | if (!viewport) { 69 | var viewportId = 70 | "viewport-" + new Date().toISOString().replace(/\D/g, ""); 71 | viewport = document.createElementNS(this.svgNS, "g"); 72 | viewport.setAttribute("id", viewportId); 73 | 74 | // Internet Explorer (all versions?) can't use childNodes, but other browsers prefer (require?) using childNodes 75 | var svgChildren = svg.childNodes || svg.children; 76 | if (!!svgChildren && svgChildren.length > 0) { 77 | for (var i = svgChildren.length; i > 0; i--) { 78 | // Move everything into viewport except defs 79 | if (svgChildren[svgChildren.length - i].nodeName !== "defs") { 80 | viewport.appendChild(svgChildren[svgChildren.length - i]); 81 | } 82 | } 83 | } 84 | svg.appendChild(viewport); 85 | } 86 | 87 | // Parse class names 88 | var classNames = []; 89 | if (viewport.getAttribute("class")) { 90 | classNames = viewport.getAttribute("class").split(" "); 91 | } 92 | 93 | // Set class (if not set already) 94 | if (!~classNames.indexOf("svg-pan-zoom_viewport")) { 95 | classNames.push("svg-pan-zoom_viewport"); 96 | viewport.setAttribute("class", classNames.join(" ")); 97 | } 98 | 99 | return viewport; 100 | }, 101 | 102 | /** 103 | * Set SVG attributes 104 | * 105 | * @param {SVGSVGElement} svg 106 | */ 107 | setupSvgAttributes: function(svg) { 108 | // Setting default attributes 109 | svg.setAttribute("xmlns", this.svgNS); 110 | svg.setAttributeNS(this.xmlnsNS, "xmlns:xlink", this.xlinkNS); 111 | svg.setAttributeNS(this.xmlnsNS, "xmlns:ev", this.evNS); 112 | 113 | // Needed for Internet Explorer, otherwise the viewport overflows 114 | if (svg.parentNode !== null) { 115 | var style = svg.getAttribute("style") || ""; 116 | if (style.toLowerCase().indexOf("overflow") === -1) { 117 | svg.setAttribute("style", "overflow: hidden; " + style); 118 | } 119 | } 120 | }, 121 | 122 | /** 123 | * How long Internet Explorer takes to finish updating its display (ms). 124 | */ 125 | internetExplorerRedisplayInterval: 300, 126 | 127 | /** 128 | * Forces the browser to redisplay all SVG elements that rely on an 129 | * element defined in a 'defs' section. It works globally, for every 130 | * available defs element on the page. 131 | * The throttling is intentionally global. 132 | * 133 | * This is only needed for IE. It is as a hack to make markers (and 'use' elements?) 134 | * visible after pan/zoom when there are multiple SVGs on the page. 135 | * See bug report: https://connect.microsoft.com/IE/feedback/details/781964/ 136 | * also see svg-pan-zoom issue: https://github.com/bumbu/svg-pan-zoom/issues/62 137 | */ 138 | refreshDefsGlobal: Utils.throttle( 139 | function() { 140 | var allDefs = document.querySelectorAll("defs"); 141 | var allDefsCount = allDefs.length; 142 | for (var i = 0; i < allDefsCount; i++) { 143 | var thisDefs = allDefs[i]; 144 | thisDefs.parentNode.insertBefore(thisDefs, thisDefs); 145 | } 146 | }, 147 | this ? this.internetExplorerRedisplayInterval : null 148 | ), 149 | 150 | /** 151 | * Sets the current transform matrix of an element 152 | * 153 | * @param {SVGElement} element 154 | * @param {SVGMatrix} matrix CTM 155 | * @param {SVGElement} defs 156 | */ 157 | setCTM: function(element, matrix, defs) { 158 | var that = this, 159 | s = 160 | "matrix(" + 161 | matrix.a + 162 | "," + 163 | matrix.b + 164 | "," + 165 | matrix.c + 166 | "," + 167 | matrix.d + 168 | "," + 169 | matrix.e + 170 | "," + 171 | matrix.f + 172 | ")"; 173 | 174 | element.setAttributeNS(null, "transform", s); 175 | if ("transform" in element.style) { 176 | element.style.transform = s; 177 | } else if ("-ms-transform" in element.style) { 178 | element.style["-ms-transform"] = s; 179 | } else if ("-webkit-transform" in element.style) { 180 | element.style["-webkit-transform"] = s; 181 | } 182 | 183 | // IE has a bug that makes markers disappear on zoom (when the matrix "a" and/or "d" elements change) 184 | // see http://stackoverflow.com/questions/17654578/svg-marker-does-not-work-in-ie9-10 185 | // and http://srndolha.wordpress.com/2013/11/25/svg-line-markers-may-disappear-in-internet-explorer-11/ 186 | if (_browser === "ie" && !!defs) { 187 | // this refresh is intended for redisplaying the SVG during zooming 188 | defs.parentNode.insertBefore(defs, defs); 189 | // this refresh is intended for redisplaying the other SVGs on a page when panning a given SVG 190 | // it is also needed for the given SVG itself, on zoomEnd, if the SVG contains any markers that 191 | // are located under any other element(s). 192 | window.setTimeout(function() { 193 | that.refreshDefsGlobal(); 194 | }, that.internetExplorerRedisplayInterval); 195 | } 196 | }, 197 | 198 | /** 199 | * Instantiate an SVGPoint object with given event coordinates 200 | * 201 | * @param {Event} evt 202 | * @param {SVGSVGElement} svg 203 | * @return {SVGPoint} point 204 | */ 205 | getEventPoint: function(evt, svg) { 206 | var point = svg.createSVGPoint(); 207 | 208 | Utils.mouseAndTouchNormalize(evt, svg); 209 | 210 | point.x = evt.clientX; 211 | point.y = evt.clientY; 212 | 213 | return point; 214 | }, 215 | 216 | /** 217 | * Get SVG center point 218 | * 219 | * @param {SVGSVGElement} svg 220 | * @return {SVGPoint} 221 | */ 222 | getSvgCenterPoint: function(svg, width, height) { 223 | return this.createSVGPoint(svg, width / 2, height / 2); 224 | }, 225 | 226 | /** 227 | * Create a SVGPoint with given x and y 228 | * 229 | * @param {SVGSVGElement} svg 230 | * @param {Number} x 231 | * @param {Number} y 232 | * @return {SVGPoint} 233 | */ 234 | createSVGPoint: function(svg, x, y) { 235 | var point = svg.createSVGPoint(); 236 | point.x = x; 237 | point.y = y; 238 | 239 | return point; 240 | } 241 | }; 242 | -------------------------------------------------------------------------------- /src/utilities.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | /** 3 | * Extends an object 4 | * 5 | * @param {Object} target object to extend 6 | * @param {Object} source object to take properties from 7 | * @return {Object} extended object 8 | */ 9 | extend: function(target, source) { 10 | target = target || {}; 11 | for (var prop in source) { 12 | // Go recursively 13 | if (this.isObject(source[prop])) { 14 | target[prop] = this.extend(target[prop], source[prop]); 15 | } else { 16 | target[prop] = source[prop]; 17 | } 18 | } 19 | return target; 20 | }, 21 | 22 | /** 23 | * Checks if an object is a DOM element 24 | * 25 | * @param {Object} o HTML element or String 26 | * @return {Boolean} returns true if object is a DOM element 27 | */ 28 | isElement: function(o) { 29 | return ( 30 | o instanceof HTMLElement || 31 | o instanceof SVGElement || 32 | o instanceof SVGSVGElement || //DOM2 33 | (o && 34 | typeof o === "object" && 35 | o !== null && 36 | o.nodeType === 1 && 37 | typeof o.nodeName === "string") 38 | ); 39 | }, 40 | 41 | /** 42 | * Checks if an object is an Object 43 | * 44 | * @param {Object} o Object 45 | * @return {Boolean} returns true if object is an Object 46 | */ 47 | isObject: function(o) { 48 | return Object.prototype.toString.call(o) === "[object Object]"; 49 | }, 50 | 51 | /** 52 | * Checks if variable is Number 53 | * 54 | * @param {Integer|Float} n 55 | * @return {Boolean} returns true if variable is Number 56 | */ 57 | isNumber: function(n) { 58 | return !isNaN(parseFloat(n)) && isFinite(n); 59 | }, 60 | 61 | /** 62 | * Search for an SVG element 63 | * 64 | * @param {Object|String} elementOrSelector DOM Element or selector String 65 | * @return {Object|Null} SVG or null 66 | */ 67 | getSvg: function(elementOrSelector) { 68 | var element, svg; 69 | 70 | if (!this.isElement(elementOrSelector)) { 71 | // If selector provided 72 | if ( 73 | typeof elementOrSelector === "string" || 74 | elementOrSelector instanceof String 75 | ) { 76 | // Try to find the element 77 | element = document.querySelector(elementOrSelector); 78 | 79 | if (!element) { 80 | throw new Error( 81 | "Provided selector did not find any elements. Selector: " + 82 | elementOrSelector 83 | ); 84 | return null; 85 | } 86 | } else { 87 | throw new Error("Provided selector is not an HTML object nor String"); 88 | return null; 89 | } 90 | } else { 91 | element = elementOrSelector; 92 | } 93 | 94 | if (element.tagName.toLowerCase() === "svg") { 95 | svg = element; 96 | } else { 97 | if (element.tagName.toLowerCase() === "object") { 98 | svg = element.contentDocument.documentElement; 99 | } else { 100 | if (element.tagName.toLowerCase() === "embed") { 101 | svg = element.getSVGDocument().documentElement; 102 | } else { 103 | if (element.tagName.toLowerCase() === "img") { 104 | throw new Error( 105 | 'Cannot script an SVG in an "img" element. Please use an "object" element or an in-line SVG.' 106 | ); 107 | } else { 108 | throw new Error("Cannot get SVG."); 109 | } 110 | return null; 111 | } 112 | } 113 | } 114 | 115 | return svg; 116 | }, 117 | 118 | /** 119 | * Attach a given context to a function 120 | * @param {Function} fn Function 121 | * @param {Object} context Context 122 | * @return {Function} Function with certain context 123 | */ 124 | proxy: function(fn, context) { 125 | return function() { 126 | return fn.apply(context, arguments); 127 | }; 128 | }, 129 | 130 | /** 131 | * Returns object type 132 | * Uses toString that returns [object SVGPoint] 133 | * And than parses object type from string 134 | * 135 | * @param {Object} o Any object 136 | * @return {String} Object type 137 | */ 138 | getType: function(o) { 139 | return Object.prototype.toString 140 | .apply(o) 141 | .replace(/^\[object\s/, "") 142 | .replace(/\]$/, ""); 143 | }, 144 | 145 | /** 146 | * If it is a touch event than add clientX and clientY to event object 147 | * 148 | * @param {Event} evt 149 | * @param {SVGSVGElement} svg 150 | */ 151 | mouseAndTouchNormalize: function(evt, svg) { 152 | // If no clientX then fallback 153 | if (evt.clientX === void 0 || evt.clientX === null) { 154 | // Fallback 155 | evt.clientX = 0; 156 | evt.clientY = 0; 157 | 158 | // If it is a touch event 159 | if (evt.touches !== void 0 && evt.touches.length) { 160 | if (evt.touches[0].clientX !== void 0) { 161 | evt.clientX = evt.touches[0].clientX; 162 | evt.clientY = evt.touches[0].clientY; 163 | } else if (evt.touches[0].pageX !== void 0) { 164 | var rect = svg.getBoundingClientRect(); 165 | 166 | evt.clientX = evt.touches[0].pageX - rect.left; 167 | evt.clientY = evt.touches[0].pageY - rect.top; 168 | } 169 | // If it is a custom event 170 | } else if (evt.originalEvent !== void 0) { 171 | if (evt.originalEvent.clientX !== void 0) { 172 | evt.clientX = evt.originalEvent.clientX; 173 | evt.clientY = evt.originalEvent.clientY; 174 | } 175 | } 176 | } 177 | }, 178 | 179 | /** 180 | * Check if an event is a double click/tap 181 | * TODO: For touch gestures use a library (hammer.js) that takes in account other events 182 | * (touchmove and touchend). It should take in account tap duration and traveled distance 183 | * 184 | * @param {Event} evt 185 | * @param {Event} prevEvt Previous Event 186 | * @return {Boolean} 187 | */ 188 | isDblClick: function(evt, prevEvt) { 189 | // Double click detected by browser 190 | if (evt.detail === 2) { 191 | return true; 192 | } 193 | // Try to compare events 194 | else if (prevEvt !== void 0 && prevEvt !== null) { 195 | var timeStampDiff = evt.timeStamp - prevEvt.timeStamp, // should be lower than 250 ms 196 | touchesDistance = Math.sqrt( 197 | Math.pow(evt.clientX - prevEvt.clientX, 2) + 198 | Math.pow(evt.clientY - prevEvt.clientY, 2) 199 | ); 200 | 201 | return timeStampDiff < 250 && touchesDistance < 10; 202 | } 203 | 204 | // Nothing found 205 | return false; 206 | }, 207 | 208 | /** 209 | * Returns current timestamp as an integer 210 | * 211 | * @return {Number} 212 | */ 213 | now: 214 | Date.now || 215 | function() { 216 | return new Date().getTime(); 217 | }, 218 | 219 | // From underscore. 220 | // Returns a function, that, when invoked, will only be triggered at most once 221 | // during a given window of time. Normally, the throttled function will run 222 | // as much as it can, without ever going more than once per `wait` duration; 223 | // but if you'd like to disable the execution on the leading edge, pass 224 | // `{leading: false}`. To disable execution on the trailing edge, ditto. 225 | throttle: function(func, wait, options) { 226 | var that = this; 227 | var context, args, result; 228 | var timeout = null; 229 | var previous = 0; 230 | if (!options) { 231 | options = {}; 232 | } 233 | var later = function() { 234 | previous = options.leading === false ? 0 : that.now(); 235 | timeout = null; 236 | result = func.apply(context, args); 237 | if (!timeout) { 238 | context = args = null; 239 | } 240 | }; 241 | return function() { 242 | var now = that.now(); 243 | if (!previous && options.leading === false) { 244 | previous = now; 245 | } 246 | var remaining = wait - (now - previous); 247 | context = this; // eslint-disable-line consistent-this 248 | args = arguments; 249 | if (remaining <= 0 || remaining > wait) { 250 | clearTimeout(timeout); 251 | timeout = null; 252 | previous = now; 253 | result = func.apply(context, args); 254 | if (!timeout) { 255 | context = args = null; 256 | } 257 | } else if (!timeout && options.trailing !== false) { 258 | timeout = setTimeout(later, remaining); 259 | } 260 | return result; 261 | }; 262 | }, 263 | 264 | /** 265 | * Create a requestAnimationFrame simulation 266 | * 267 | * @param {Number|String} refreshRate 268 | * @return {Function} 269 | */ 270 | createRequestAnimationFrame: function(refreshRate) { 271 | var timeout = null; 272 | 273 | // Convert refreshRate to timeout 274 | if (refreshRate !== "auto" && refreshRate < 60 && refreshRate > 1) { 275 | timeout = Math.floor(1000 / refreshRate); 276 | } 277 | 278 | if (timeout === null) { 279 | return window.requestAnimationFrame || requestTimeout(33); 280 | } else { 281 | return requestTimeout(timeout); 282 | } 283 | } 284 | }; 285 | 286 | /** 287 | * Create a callback that will execute after a given timeout 288 | * 289 | * @param {Function} timeout 290 | * @return {Function} 291 | */ 292 | function requestTimeout(timeout) { 293 | return function(callback) { 294 | window.setTimeout(callback, timeout); 295 | }; 296 | } 297 | -------------------------------------------------------------------------------- /src/control-icons.js: -------------------------------------------------------------------------------- 1 | var SvgUtils = require("./svg-utilities"); 2 | 3 | module.exports = { 4 | enable: function(instance) { 5 | // Select (and create if necessary) defs 6 | var defs = instance.svg.querySelector("defs"); 7 | if (!defs) { 8 | defs = document.createElementNS(SvgUtils.svgNS, "defs"); 9 | instance.svg.appendChild(defs); 10 | } 11 | 12 | // Check for style element, and create it if it doesn't exist 13 | var styleEl = defs.querySelector("style#svg-pan-zoom-controls-styles"); 14 | if (!styleEl) { 15 | var style = document.createElementNS(SvgUtils.svgNS, "style"); 16 | style.setAttribute("id", "svg-pan-zoom-controls-styles"); 17 | style.setAttribute("type", "text/css"); 18 | style.textContent = 19 | ".svg-pan-zoom-control { cursor: pointer; fill: black; fill-opacity: 0.333; } .svg-pan-zoom-control:hover { fill-opacity: 0.8; } .svg-pan-zoom-control-background { fill: white; fill-opacity: 0.5; } .svg-pan-zoom-control-background { fill-opacity: 0.8; }"; 20 | defs.appendChild(style); 21 | } 22 | 23 | // Zoom Group 24 | var zoomGroup = document.createElementNS(SvgUtils.svgNS, "g"); 25 | zoomGroup.setAttribute("id", "svg-pan-zoom-controls"); 26 | zoomGroup.setAttribute( 27 | "transform", 28 | "translate(" + 29 | (instance.width - 70) + 30 | " " + 31 | (instance.height - 76) + 32 | ") scale(0.75)" 33 | ); 34 | zoomGroup.setAttribute("class", "svg-pan-zoom-control"); 35 | 36 | // Control elements 37 | zoomGroup.appendChild(this._createZoomIn(instance)); 38 | zoomGroup.appendChild(this._createZoomReset(instance)); 39 | zoomGroup.appendChild(this._createZoomOut(instance)); 40 | 41 | // Finally append created element 42 | instance.svg.appendChild(zoomGroup); 43 | 44 | // Cache control instance 45 | instance.controlIcons = zoomGroup; 46 | }, 47 | 48 | _createZoomIn: function(instance) { 49 | var zoomIn = document.createElementNS(SvgUtils.svgNS, "g"); 50 | zoomIn.setAttribute("id", "svg-pan-zoom-zoom-in"); 51 | zoomIn.setAttribute("transform", "translate(30.5 5) scale(0.015)"); 52 | zoomIn.setAttribute("class", "svg-pan-zoom-control"); 53 | zoomIn.addEventListener( 54 | "click", 55 | function() { 56 | instance.getPublicInstance().zoomIn(); 57 | }, 58 | false 59 | ); 60 | zoomIn.addEventListener( 61 | "touchstart", 62 | function() { 63 | instance.getPublicInstance().zoomIn(); 64 | }, 65 | false 66 | ); 67 | 68 | var zoomInBackground = document.createElementNS(SvgUtils.svgNS, "rect"); // TODO change these background space fillers to rounded rectangles so they look prettier 69 | zoomInBackground.setAttribute("x", "0"); 70 | zoomInBackground.setAttribute("y", "0"); 71 | zoomInBackground.setAttribute("width", "1500"); // larger than expected because the whole group is transformed to scale down 72 | zoomInBackground.setAttribute("height", "1400"); 73 | zoomInBackground.setAttribute("class", "svg-pan-zoom-control-background"); 74 | zoomIn.appendChild(zoomInBackground); 75 | 76 | var zoomInShape = document.createElementNS(SvgUtils.svgNS, "path"); 77 | zoomInShape.setAttribute( 78 | "d", 79 | "M1280 576v128q0 26 -19 45t-45 19h-320v320q0 26 -19 45t-45 19h-128q-26 0 -45 -19t-19 -45v-320h-320q-26 0 -45 -19t-19 -45v-128q0 -26 19 -45t45 -19h320v-320q0 -26 19 -45t45 -19h128q26 0 45 19t19 45v320h320q26 0 45 19t19 45zM1536 1120v-960 q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" 80 | ); 81 | zoomInShape.setAttribute("class", "svg-pan-zoom-control-element"); 82 | zoomIn.appendChild(zoomInShape); 83 | 84 | return zoomIn; 85 | }, 86 | 87 | _createZoomReset: function(instance) { 88 | // reset 89 | var resetPanZoomControl = document.createElementNS(SvgUtils.svgNS, "g"); 90 | resetPanZoomControl.setAttribute("id", "svg-pan-zoom-reset-pan-zoom"); 91 | resetPanZoomControl.setAttribute("transform", "translate(5 35) scale(0.4)"); 92 | resetPanZoomControl.setAttribute("class", "svg-pan-zoom-control"); 93 | resetPanZoomControl.addEventListener( 94 | "click", 95 | function() { 96 | instance.getPublicInstance().reset(); 97 | }, 98 | false 99 | ); 100 | resetPanZoomControl.addEventListener( 101 | "touchstart", 102 | function() { 103 | instance.getPublicInstance().reset(); 104 | }, 105 | false 106 | ); 107 | 108 | var resetPanZoomControlBackground = document.createElementNS( 109 | SvgUtils.svgNS, 110 | "rect" 111 | ); // TODO change these background space fillers to rounded rectangles so they look prettier 112 | resetPanZoomControlBackground.setAttribute("x", "2"); 113 | resetPanZoomControlBackground.setAttribute("y", "2"); 114 | resetPanZoomControlBackground.setAttribute("width", "182"); // larger than expected because the whole group is transformed to scale down 115 | resetPanZoomControlBackground.setAttribute("height", "58"); 116 | resetPanZoomControlBackground.setAttribute( 117 | "class", 118 | "svg-pan-zoom-control-background" 119 | ); 120 | resetPanZoomControl.appendChild(resetPanZoomControlBackground); 121 | 122 | var resetPanZoomControlShape1 = document.createElementNS( 123 | SvgUtils.svgNS, 124 | "path" 125 | ); 126 | resetPanZoomControlShape1.setAttribute( 127 | "d", 128 | "M33.051,20.632c-0.742-0.406-1.854-0.609-3.338-0.609h-7.969v9.281h7.769c1.543,0,2.701-0.188,3.473-0.562c1.365-0.656,2.048-1.953,2.048-3.891C35.032,22.757,34.372,21.351,33.051,20.632z" 129 | ); 130 | resetPanZoomControlShape1.setAttribute( 131 | "class", 132 | "svg-pan-zoom-control-element" 133 | ); 134 | resetPanZoomControl.appendChild(resetPanZoomControlShape1); 135 | 136 | var resetPanZoomControlShape2 = document.createElementNS( 137 | SvgUtils.svgNS, 138 | "path" 139 | ); 140 | resetPanZoomControlShape2.setAttribute( 141 | "d", 142 | "M170.231,0.5H15.847C7.102,0.5,0.5,5.708,0.5,11.84v38.861C0.5,56.833,7.102,61.5,15.847,61.5h154.384c8.745,0,15.269-4.667,15.269-10.798V11.84C185.5,5.708,178.976,0.5,170.231,0.5z M42.837,48.569h-7.969c-0.219-0.766-0.375-1.383-0.469-1.852c-0.188-0.969-0.289-1.961-0.305-2.977l-0.047-3.211c-0.03-2.203-0.41-3.672-1.142-4.406c-0.732-0.734-2.103-1.102-4.113-1.102h-7.05v13.547h-7.055V14.022h16.524c2.361,0.047,4.178,0.344,5.45,0.891c1.272,0.547,2.351,1.352,3.234,2.414c0.731,0.875,1.31,1.844,1.737,2.906s0.64,2.273,0.64,3.633c0,1.641-0.414,3.254-1.242,4.84s-2.195,2.707-4.102,3.363c1.594,0.641,2.723,1.551,3.387,2.73s0.996,2.98,0.996,5.402v2.32c0,1.578,0.063,2.648,0.19,3.211c0.19,0.891,0.635,1.547,1.333,1.969V48.569z M75.579,48.569h-26.18V14.022h25.336v6.117H56.454v7.336h16.781v6H56.454v8.883h19.125V48.569z M104.497,46.331c-2.44,2.086-5.887,3.129-10.34,3.129c-4.548,0-8.125-1.027-10.731-3.082s-3.909-4.879-3.909-8.473h6.891c0.224,1.578,0.662,2.758,1.316,3.539c1.196,1.422,3.246,2.133,6.15,2.133c1.739,0,3.151-0.188,4.236-0.562c2.058-0.719,3.087-2.055,3.087-4.008c0-1.141-0.504-2.023-1.512-2.648c-1.008-0.609-2.607-1.148-4.796-1.617l-3.74-0.82c-3.676-0.812-6.201-1.695-7.576-2.648c-2.328-1.594-3.492-4.086-3.492-7.477c0-3.094,1.139-5.664,3.417-7.711s5.623-3.07,10.036-3.07c3.685,0,6.829,0.965,9.431,2.895c2.602,1.93,3.966,4.73,4.093,8.402h-6.938c-0.128-2.078-1.057-3.555-2.787-4.43c-1.154-0.578-2.587-0.867-4.301-0.867c-1.907,0-3.428,0.375-4.565,1.125c-1.138,0.75-1.706,1.797-1.706,3.141c0,1.234,0.561,2.156,1.682,2.766c0.721,0.406,2.25,0.883,4.589,1.43l6.063,1.43c2.657,0.625,4.648,1.461,5.975,2.508c2.059,1.625,3.089,3.977,3.089,7.055C108.157,41.624,106.937,44.245,104.497,46.331z M139.61,48.569h-26.18V14.022h25.336v6.117h-18.281v7.336h16.781v6h-16.781v8.883h19.125V48.569z M170.337,20.14h-10.336v28.43h-7.266V20.14h-10.383v-6.117h27.984V20.14z" 143 | ); 144 | resetPanZoomControlShape2.setAttribute( 145 | "class", 146 | "svg-pan-zoom-control-element" 147 | ); 148 | resetPanZoomControl.appendChild(resetPanZoomControlShape2); 149 | 150 | return resetPanZoomControl; 151 | }, 152 | 153 | _createZoomOut: function(instance) { 154 | // zoom out 155 | var zoomOut = document.createElementNS(SvgUtils.svgNS, "g"); 156 | zoomOut.setAttribute("id", "svg-pan-zoom-zoom-out"); 157 | zoomOut.setAttribute("transform", "translate(30.5 70) scale(0.015)"); 158 | zoomOut.setAttribute("class", "svg-pan-zoom-control"); 159 | zoomOut.addEventListener( 160 | "click", 161 | function() { 162 | instance.getPublicInstance().zoomOut(); 163 | }, 164 | false 165 | ); 166 | zoomOut.addEventListener( 167 | "touchstart", 168 | function() { 169 | instance.getPublicInstance().zoomOut(); 170 | }, 171 | false 172 | ); 173 | 174 | var zoomOutBackground = document.createElementNS(SvgUtils.svgNS, "rect"); // TODO change these background space fillers to rounded rectangles so they look prettier 175 | zoomOutBackground.setAttribute("x", "0"); 176 | zoomOutBackground.setAttribute("y", "0"); 177 | zoomOutBackground.setAttribute("width", "1500"); // larger than expected because the whole group is transformed to scale down 178 | zoomOutBackground.setAttribute("height", "1400"); 179 | zoomOutBackground.setAttribute("class", "svg-pan-zoom-control-background"); 180 | zoomOut.appendChild(zoomOutBackground); 181 | 182 | var zoomOutShape = document.createElementNS(SvgUtils.svgNS, "path"); 183 | zoomOutShape.setAttribute( 184 | "d", 185 | "M1280 576v128q0 26 -19 45t-45 19h-896q-26 0 -45 -19t-19 -45v-128q0 -26 19 -45t45 -19h896q26 0 45 19t19 45zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5 t84.5 -203.5z" 186 | ); 187 | zoomOutShape.setAttribute("class", "svg-pan-zoom-control-element"); 188 | zoomOut.appendChild(zoomOutShape); 189 | 190 | return zoomOut; 191 | }, 192 | 193 | disable: function(instance) { 194 | if (instance.controlIcons) { 195 | instance.controlIcons.parentNode.removeChild(instance.controlIcons); 196 | instance.controlIcons = null; 197 | } 198 | } 199 | }; 200 | -------------------------------------------------------------------------------- /svg-pan-zoom-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 12 | 13 | 14 | 15 | 16 | 17 | 22 | 23 | 26 | 27 | 28 | 29 | 30 | 33 | 35 | 37 | 43 | 44 | 49 | 54 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 78 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 110 | 111 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 124 | 125 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /src/shadow-viewport.js: -------------------------------------------------------------------------------- 1 | var SvgUtils = require("./svg-utilities"), 2 | Utils = require("./utilities"); 3 | 4 | var ShadowViewport = function(viewport, options) { 5 | this.init(viewport, options); 6 | }; 7 | 8 | /** 9 | * Initialization 10 | * 11 | * @param {SVGElement} viewport 12 | * @param {Object} options 13 | */ 14 | ShadowViewport.prototype.init = function(viewport, options) { 15 | // DOM Elements 16 | this.viewport = viewport; 17 | this.options = options; 18 | 19 | // State cache 20 | this.originalState = { zoom: 1, x: 0, y: 0 }; 21 | this.activeState = { zoom: 1, x: 0, y: 0 }; 22 | 23 | this.updateCTMCached = Utils.proxy(this.updateCTM, this); 24 | 25 | // Create a custom requestAnimationFrame taking in account refreshRate 26 | this.requestAnimationFrame = Utils.createRequestAnimationFrame( 27 | this.options.refreshRate 28 | ); 29 | 30 | // ViewBox 31 | this.viewBox = { x: 0, y: 0, width: 0, height: 0 }; 32 | this.cacheViewBox(); 33 | 34 | // Process CTM 35 | var newCTM = this.processCTM(); 36 | 37 | // Update viewport CTM and cache zoom and pan 38 | this.setCTM(newCTM); 39 | 40 | // Update CTM in this frame 41 | this.updateCTM(); 42 | }; 43 | 44 | /** 45 | * Cache initial viewBox value 46 | * If no viewBox is defined, then use viewport size/position instead for viewBox values 47 | */ 48 | ShadowViewport.prototype.cacheViewBox = function() { 49 | var svgViewBox = this.options.svg.getAttribute("viewBox"); 50 | 51 | if (svgViewBox) { 52 | var viewBoxValues = svgViewBox 53 | .split(/[\s\,]/) 54 | .filter(function(v) { 55 | return v; 56 | }) 57 | .map(parseFloat); 58 | 59 | // Cache viewbox x and y offset 60 | this.viewBox.x = viewBoxValues[0]; 61 | this.viewBox.y = viewBoxValues[1]; 62 | this.viewBox.width = viewBoxValues[2]; 63 | this.viewBox.height = viewBoxValues[3]; 64 | 65 | var zoom = Math.min( 66 | this.options.width / this.viewBox.width, 67 | this.options.height / this.viewBox.height 68 | ); 69 | 70 | // Update active state 71 | this.activeState.zoom = zoom; 72 | this.activeState.x = (this.options.width - this.viewBox.width * zoom) / 2; 73 | this.activeState.y = (this.options.height - this.viewBox.height * zoom) / 2; 74 | 75 | // Force updating CTM 76 | this.updateCTMOnNextFrame(); 77 | 78 | this.options.svg.removeAttribute("viewBox"); 79 | } else { 80 | this.simpleViewBoxCache(); 81 | } 82 | }; 83 | 84 | /** 85 | * Recalculate viewport sizes and update viewBox cache 86 | */ 87 | ShadowViewport.prototype.simpleViewBoxCache = function() { 88 | var bBox = this.viewport.getBBox(); 89 | 90 | this.viewBox.x = bBox.x; 91 | this.viewBox.y = bBox.y; 92 | this.viewBox.width = bBox.width; 93 | this.viewBox.height = bBox.height; 94 | }; 95 | 96 | /** 97 | * Returns a viewbox object. Safe to alter 98 | * 99 | * @return {Object} viewbox object 100 | */ 101 | ShadowViewport.prototype.getViewBox = function() { 102 | return Utils.extend({}, this.viewBox); 103 | }; 104 | 105 | /** 106 | * Get initial zoom and pan values. Save them into originalState 107 | * Parses viewBox attribute to alter initial sizes 108 | * 109 | * @return {CTM} CTM object based on options 110 | */ 111 | ShadowViewport.prototype.processCTM = function() { 112 | var newCTM = this.getCTM(); 113 | 114 | if (this.options.fit || this.options.contain) { 115 | var newScale; 116 | if (this.options.fit) { 117 | newScale = Math.min( 118 | this.options.width / this.viewBox.width, 119 | this.options.height / this.viewBox.height 120 | ); 121 | } else { 122 | newScale = Math.max( 123 | this.options.width / this.viewBox.width, 124 | this.options.height / this.viewBox.height 125 | ); 126 | } 127 | 128 | newCTM.a = newScale; //x-scale 129 | newCTM.d = newScale; //y-scale 130 | newCTM.e = -this.viewBox.x * newScale; //x-transform 131 | newCTM.f = -this.viewBox.y * newScale; //y-transform 132 | } 133 | 134 | if (this.options.center) { 135 | var offsetX = 136 | (this.options.width - 137 | (this.viewBox.width + this.viewBox.x * 2) * newCTM.a) * 138 | 0.5, 139 | offsetY = 140 | (this.options.height - 141 | (this.viewBox.height + this.viewBox.y * 2) * newCTM.a) * 142 | 0.5; 143 | 144 | newCTM.e = offsetX; 145 | newCTM.f = offsetY; 146 | } 147 | 148 | // Cache initial values. Based on activeState and fix+center opitons 149 | this.originalState.zoom = newCTM.a; 150 | this.originalState.x = newCTM.e; 151 | this.originalState.y = newCTM.f; 152 | 153 | return newCTM; 154 | }; 155 | 156 | /** 157 | * Return originalState object. Safe to alter 158 | * 159 | * @return {Object} 160 | */ 161 | ShadowViewport.prototype.getOriginalState = function() { 162 | return Utils.extend({}, this.originalState); 163 | }; 164 | 165 | /** 166 | * Return actualState object. Safe to alter 167 | * 168 | * @return {Object} 169 | */ 170 | ShadowViewport.prototype.getState = function() { 171 | return Utils.extend({}, this.activeState); 172 | }; 173 | 174 | /** 175 | * Get zoom scale 176 | * 177 | * @return {Float} zoom scale 178 | */ 179 | ShadowViewport.prototype.getZoom = function() { 180 | return this.activeState.zoom; 181 | }; 182 | 183 | /** 184 | * Get zoom scale for pubilc usage 185 | * 186 | * @return {Float} zoom scale 187 | */ 188 | ShadowViewport.prototype.getRelativeZoom = function() { 189 | return this.activeState.zoom / this.originalState.zoom; 190 | }; 191 | 192 | /** 193 | * Compute zoom scale for pubilc usage 194 | * 195 | * @return {Float} zoom scale 196 | */ 197 | ShadowViewport.prototype.computeRelativeZoom = function(scale) { 198 | return scale / this.originalState.zoom; 199 | }; 200 | 201 | /** 202 | * Get pan 203 | * 204 | * @return {Object} 205 | */ 206 | ShadowViewport.prototype.getPan = function() { 207 | return { x: this.activeState.x, y: this.activeState.y }; 208 | }; 209 | 210 | /** 211 | * Return cached viewport CTM value that can be safely modified 212 | * 213 | * @return {SVGMatrix} 214 | */ 215 | ShadowViewport.prototype.getCTM = function() { 216 | var safeCTM = this.options.svg.createSVGMatrix(); 217 | 218 | // Copy values manually as in FF they are not itterable 219 | safeCTM.a = this.activeState.zoom; 220 | safeCTM.b = 0; 221 | safeCTM.c = 0; 222 | safeCTM.d = this.activeState.zoom; 223 | safeCTM.e = this.activeState.x; 224 | safeCTM.f = this.activeState.y; 225 | 226 | return safeCTM; 227 | }; 228 | 229 | /** 230 | * Set a new CTM 231 | * 232 | * @param {SVGMatrix} newCTM 233 | */ 234 | ShadowViewport.prototype.setCTM = function(newCTM) { 235 | var willZoom = this.isZoomDifferent(newCTM), 236 | willPan = this.isPanDifferent(newCTM); 237 | 238 | if (willZoom || willPan) { 239 | // Before zoom 240 | if (willZoom) { 241 | // If returns false then cancel zooming 242 | if ( 243 | this.options.beforeZoom( 244 | this.getRelativeZoom(), 245 | this.computeRelativeZoom(newCTM.a) 246 | ) === false 247 | ) { 248 | newCTM.a = newCTM.d = this.activeState.zoom; 249 | willZoom = false; 250 | } else { 251 | this.updateCache(newCTM); 252 | this.options.onZoom(this.getRelativeZoom()); 253 | } 254 | } 255 | 256 | // Before pan 257 | if (willPan) { 258 | var preventPan = this.options.beforePan(this.getPan(), { 259 | x: newCTM.e, 260 | y: newCTM.f 261 | }), 262 | // If prevent pan is an object 263 | preventPanX = false, 264 | preventPanY = false; 265 | 266 | // If prevent pan is Boolean false 267 | if (preventPan === false) { 268 | // Set x and y same as before 269 | newCTM.e = this.getPan().x; 270 | newCTM.f = this.getPan().y; 271 | 272 | preventPanX = preventPanY = true; 273 | } else if (Utils.isObject(preventPan)) { 274 | // Check for X axes attribute 275 | if (preventPan.x === false) { 276 | // Prevent panning on x axes 277 | newCTM.e = this.getPan().x; 278 | preventPanX = true; 279 | } else if (Utils.isNumber(preventPan.x)) { 280 | // Set a custom pan value 281 | newCTM.e = preventPan.x; 282 | } 283 | 284 | // Check for Y axes attribute 285 | if (preventPan.y === false) { 286 | // Prevent panning on x axes 287 | newCTM.f = this.getPan().y; 288 | preventPanY = true; 289 | } else if (Utils.isNumber(preventPan.y)) { 290 | // Set a custom pan value 291 | newCTM.f = preventPan.y; 292 | } 293 | } 294 | 295 | // Update willPan flag 296 | // Check if newCTM is still different 297 | if ((preventPanX && preventPanY) || !this.isPanDifferent(newCTM)) { 298 | willPan = false; 299 | } else { 300 | this.updateCache(newCTM); 301 | this.options.onPan(this.getPan()); 302 | } 303 | } 304 | 305 | // Check again if should zoom or pan 306 | if (willZoom || willPan) { 307 | this.updateCTMOnNextFrame(); 308 | } 309 | } 310 | }; 311 | 312 | ShadowViewport.prototype.isZoomDifferent = function(newCTM) { 313 | return this.activeState.zoom !== newCTM.a; 314 | }; 315 | 316 | ShadowViewport.prototype.isPanDifferent = function(newCTM) { 317 | return this.activeState.x !== newCTM.e || this.activeState.y !== newCTM.f; 318 | }; 319 | 320 | /** 321 | * Update cached CTM and active state 322 | * 323 | * @param {SVGMatrix} newCTM 324 | */ 325 | ShadowViewport.prototype.updateCache = function(newCTM) { 326 | this.activeState.zoom = newCTM.a; 327 | this.activeState.x = newCTM.e; 328 | this.activeState.y = newCTM.f; 329 | }; 330 | 331 | ShadowViewport.prototype.pendingUpdate = false; 332 | 333 | /** 334 | * Place a request to update CTM on next Frame 335 | */ 336 | ShadowViewport.prototype.updateCTMOnNextFrame = function() { 337 | if (!this.pendingUpdate) { 338 | // Lock 339 | this.pendingUpdate = true; 340 | 341 | // Throttle next update 342 | this.requestAnimationFrame.call(window, this.updateCTMCached); 343 | } 344 | }; 345 | 346 | /** 347 | * Update viewport CTM with cached CTM 348 | */ 349 | ShadowViewport.prototype.updateCTM = function() { 350 | var ctm = this.getCTM(); 351 | 352 | // Updates SVG element 353 | SvgUtils.setCTM(this.viewport, ctm, this.defs); 354 | 355 | // Free the lock 356 | this.pendingUpdate = false; 357 | 358 | // Notify about the update 359 | if (this.options.onUpdatedCTM) { 360 | this.options.onUpdatedCTM(ctm); 361 | } 362 | }; 363 | 364 | module.exports = function(viewport, options) { 365 | return new ShadowViewport(viewport, options); 366 | }; 367 | -------------------------------------------------------------------------------- /tests/test_api.js: -------------------------------------------------------------------------------- 1 | var svg, 2 | svgSelector = "#test-inline", 3 | svgSelectorViewbox = "#test-viewbox", 4 | svgSelectorTransform = "#test-transform", 5 | svgSelectorViewboxTransform = "#test-viewbox-transform", 6 | instance; 7 | 8 | var initSvgPanZoom = function(options, alternativeSelector) { 9 | if (options) { 10 | return svgPanZoom(alternativeSelector || svgSelector, options); 11 | } else { 12 | return svgPanZoom(alternativeSelector || svgSelector); 13 | } 14 | }; 15 | 16 | /** 17 | * Compare numbers taking in account an error 18 | * 19 | * @param {Float} number 20 | * @param {Float} expected 21 | * @param {Float} error Optional 22 | * @param {String} message Optional 23 | */ 24 | var close = (QUnit.assert.close = function(number, expected, error, message) { 25 | if (error === void 0 || error === null) { 26 | error = 0.0001; // default error 27 | } 28 | 29 | /* eslint-disable eqeqeq */ 30 | var result = 31 | number == expected || 32 | (number < expected + error && number > expected - error) || 33 | false; 34 | /* eslint-enable eqeqeq */ 35 | 36 | QUnit.push(result, number, expected, message); 37 | }); 38 | 39 | module("Test API", { 40 | setup: function() {}, 41 | teardown: function() { 42 | instance && instance.destroy && instance.destroy(); 43 | } 44 | }); 45 | 46 | /** 47 | * Pan state (enabled, disabled) 48 | */ 49 | 50 | test("by default pan should be enabled", function() { 51 | expect(1); 52 | instance = initSvgPanZoom(); 53 | 54 | equal(instance.isPanEnabled(), true); 55 | }); 56 | 57 | test("disable pan via options", function() { 58 | expect(1); 59 | instance = initSvgPanZoom({ panEnabled: false }); 60 | 61 | equal(instance.isPanEnabled(), false); 62 | }); 63 | 64 | test("disable and enable pan via API", function() { 65 | expect(2); 66 | instance = initSvgPanZoom(); 67 | 68 | instance.disablePan(); 69 | equal(instance.isPanEnabled(), false); 70 | 71 | instance.enablePan(); 72 | equal(instance.isPanEnabled(), true); 73 | }); 74 | 75 | /** 76 | * Zoom state (enabled, disabled) 77 | */ 78 | 79 | test("by default zoom should be enabled", function() { 80 | expect(1); 81 | instance = initSvgPanZoom(); 82 | 83 | equal(instance.isZoomEnabled(), true); 84 | }); 85 | 86 | test("disable zoom via options", function() { 87 | expect(1); 88 | instance = initSvgPanZoom({ zoomEnabled: false }); 89 | 90 | equal(instance.isZoomEnabled(), false); 91 | }); 92 | 93 | test("disable and enable zoom via API", function() { 94 | expect(2); 95 | instance = initSvgPanZoom(); 96 | 97 | instance.disableZoom(); 98 | equal(instance.isZoomEnabled(), false); 99 | 100 | instance.enableZoom(); 101 | equal(instance.isZoomEnabled(), true); 102 | }); 103 | 104 | /** 105 | * Controls state (enabled, disabled) 106 | */ 107 | 108 | test("by default controls are disabled", function() { 109 | expect(1); 110 | instance = initSvgPanZoom(); 111 | 112 | equal(instance.isControlIconsEnabled(), false); 113 | }); 114 | 115 | test("enable controls via opions", function() { 116 | expect(1); 117 | instance = initSvgPanZoom({ controlIconsEnabled: true }); 118 | 119 | equal(instance.isControlIconsEnabled(), true); 120 | }); 121 | 122 | test("disable and enable controls via API", function() { 123 | expect(2); 124 | instance = initSvgPanZoom(); 125 | 126 | instance.enableControlIcons(); 127 | equal(instance.isControlIconsEnabled(), true); 128 | 129 | instance.disableControlIcons(); 130 | equal(instance.isControlIconsEnabled(), false); 131 | }); 132 | 133 | /** 134 | * Double click zoom state (enabled, disabled) 135 | */ 136 | 137 | test("by default double click zoom is enabled", function() { 138 | expect(1); 139 | instance = initSvgPanZoom(); 140 | 141 | equal(instance.isDblClickZoomEnabled(), true); 142 | }); 143 | 144 | test("disable double click zoom via options", function() { 145 | expect(1); 146 | instance = initSvgPanZoom({ dblClickZoomEnabled: false }); 147 | 148 | equal(instance.isDblClickZoomEnabled(), false); 149 | }); 150 | 151 | test("disable and enable double click zoom via API", function() { 152 | expect(2); 153 | instance = initSvgPanZoom(); 154 | 155 | instance.disableDblClickZoom(); 156 | equal(instance.isDblClickZoomEnabled(), false); 157 | 158 | instance.enableDblClickZoom(); 159 | equal(instance.isDblClickZoomEnabled(), true); 160 | }); 161 | 162 | /** 163 | * Mouse wheel zoom state (enabled, disabled) 164 | */ 165 | 166 | test("by default mouse wheel zoom is enabled", function() { 167 | expect(1); 168 | instance = initSvgPanZoom(); 169 | 170 | equal(instance.isMouseWheelZoomEnabled(), true); 171 | }); 172 | 173 | test("disable mouse wheel zoom via options", function() { 174 | expect(1); 175 | instance = initSvgPanZoom({ mouseWheelZoomEnabled: false }); 176 | 177 | equal(instance.isMouseWheelZoomEnabled(), false); 178 | }); 179 | 180 | test("disable and enable mouse wheel zoom via API", function() { 181 | expect(2); 182 | instance = initSvgPanZoom(); 183 | 184 | instance.disableMouseWheelZoom(); 185 | equal(instance.isMouseWheelZoomEnabled(), false); 186 | 187 | instance.enableMouseWheelZoom(); 188 | equal(instance.isMouseWheelZoomEnabled(), true); 189 | }); 190 | 191 | /** 192 | * Pan 193 | */ 194 | 195 | test("pan", function() { 196 | expect(1); 197 | instance = initSvgPanZoom(); 198 | 199 | instance.pan({ x: 100, y: 300 }); 200 | 201 | deepEqual(instance.getPan(), { 202 | x: 100, 203 | y: 300 204 | }); 205 | }); 206 | 207 | test("pan through API should work even if pan is disabled", function() { 208 | expect(1); 209 | instance = initSvgPanZoom({ panEnabled: false }); 210 | 211 | instance.pan({ x: 100, y: 300 }); 212 | 213 | deepEqual(instance.getPan(), { 214 | x: 100, 215 | y: 300 216 | }); 217 | }); 218 | 219 | test("pan by", function() { 220 | expect(1); 221 | instance = initSvgPanZoom(); 222 | 223 | var initialPan = instance.getPan(); 224 | 225 | instance.panBy({ x: 100, y: 300 }); 226 | 227 | deepEqual(instance.getPan(), { 228 | x: initialPan.x + 100, 229 | y: initialPan.y + 300 230 | }); 231 | }); 232 | 233 | /** 234 | * Pan callbacks 235 | */ 236 | 237 | test("before pan", function() { 238 | expect(1); 239 | instance = initSvgPanZoom(); 240 | 241 | var initialPan = instance.getPan(); 242 | 243 | instance.setBeforePan(function(point) { 244 | deepEqual(point, initialPan); 245 | }); 246 | 247 | instance.pan({ x: 100, y: 300 }); 248 | 249 | // Remove beforePan as it will be called on destroy 250 | instance.setBeforePan(null); 251 | 252 | // Pan one more time to test if it is really removed 253 | instance.pan({ x: 50, y: 150 }); 254 | }); 255 | 256 | test("don't trigger on pan if canceld by before pan", function() { 257 | expect(1); 258 | instance = initSvgPanZoom({ 259 | onPan: function() { 260 | QUnit.ok(true, "onUpdatedCTM got called"); 261 | } 262 | }); 263 | 264 | instance.panBy({ x: 100, y: 300 }); 265 | 266 | instance.setBeforePan(function(oldPan, newPan) { 267 | return false; 268 | }); 269 | 270 | instance.panBy({ x: 100, y: 300 }); 271 | }); 272 | 273 | test("don't trigger on pan if canceld by before pan for each axis separately", function() { 274 | expect(1); 275 | instance = initSvgPanZoom({ 276 | onPan: function() { 277 | QUnit.ok(true, "onUpdatedCTM got called"); 278 | } 279 | }); 280 | 281 | instance.panBy({ x: 100, y: 300 }); 282 | 283 | instance.setBeforePan(function(oldPan, newPan) { 284 | return { x: false, y: false }; 285 | }); 286 | 287 | instance.panBy({ x: 100, y: 300 }); 288 | }); 289 | 290 | test("don't trigger on pan if canceld by before pan for each axis separately", function() { 291 | expect(1); 292 | instance = initSvgPanZoom({ 293 | onPan: function() { 294 | QUnit.ok(true, "onUpdatedCTM got called"); 295 | } 296 | }); 297 | 298 | instance.panBy({ x: 100, y: 300 }); 299 | 300 | instance.setBeforePan(function(oldPan, newPan) { 301 | return { x: false, y: false }; 302 | }); 303 | 304 | instance.panBy({ x: 100, y: 300 }); 305 | }); 306 | 307 | test("on pan", function() { 308 | expect(1); 309 | instance = initSvgPanZoom(); 310 | 311 | instance.setOnPan(function(point) { 312 | deepEqual(point, { x: 100, y: 300 }); 313 | }); 314 | 315 | instance.pan({ x: 100, y: 300 }); 316 | 317 | // Remove onPan as it will be called on destroy 318 | instance.setOnPan(null); 319 | 320 | // Pan one more time to test if it is really removed 321 | instance.pan({ x: 50, y: 150 }); 322 | }); 323 | 324 | test("change only X axis when Y axis change is prevented with before pan", function() { 325 | expect(2); 326 | instance = initSvgPanZoom(); 327 | var initialPan = instance.getPan(); 328 | 329 | instance.setOnPan(function(newPan) { 330 | notEqual(newPan.x, initialPan.x); 331 | equal(newPan.y, initialPan.y); 332 | }); 333 | 334 | instance.setBeforePan(function(oldPan, newPan) { 335 | return { y: false }; 336 | }); 337 | 338 | instance.panBy({ x: 100, y: 300 }); 339 | 340 | // Remove onPan as it will be called on destroy 341 | instance.setOnPan(null); 342 | }); 343 | 344 | test("change pan values from before pan", function() { 345 | expect(1); 346 | instance = initSvgPanZoom(); 347 | 348 | instance.setOnPan(function(newPan) { 349 | deepEqual(newPan, { x: 1, y: 2 }); 350 | }); 351 | 352 | instance.setBeforePan(function(oldPan, newPan) { 353 | return { x: 1, y: 2 }; 354 | }); 355 | 356 | instance.panBy({ x: 100, y: 300 }); 357 | 358 | // Remove onPan as it will be called on destroy 359 | instance.setOnPan(null); 360 | }); 361 | 362 | test("don't pan if before pan makes the pan unnecessary", function() { 363 | expect(0); 364 | instance = initSvgPanZoom(); 365 | var initialPan = instance.getPan(); 366 | 367 | instance.setOnPan(function() { 368 | QUnit.ok(true, "onUpdatedCTM got called"); 369 | }); 370 | 371 | instance.setBeforePan(function(oldPan, newPan) { 372 | return { x: false, y: initialPan.y }; 373 | }); 374 | 375 | instance.panBy({ x: 100, y: 300 }); 376 | 377 | // Remove onPan as it will be called on destroy 378 | instance.setOnPan(null); 379 | }); 380 | 381 | /** 382 | * Zoom 383 | */ 384 | 385 | test("zoom", function() { 386 | expect(1); 387 | instance = initSvgPanZoom(); 388 | 389 | instance.zoom(3); 390 | 391 | equal(instance.getZoom(), 3); 392 | }); 393 | 394 | test("zoom by", function() { 395 | expect(1); 396 | instance = initSvgPanZoom(); 397 | 398 | var initialZoom = instance.getZoom(); 399 | 400 | instance.zoomBy(2); 401 | 402 | equal(instance.getZoom(), initialZoom * 2); 403 | }); 404 | 405 | test("zoom at point", function() { 406 | expect(2); 407 | instance = initSvgPanZoom({ fit: false }); 408 | 409 | instance.zoomAtPoint(2, { x: 200, y: 100 }); 410 | 411 | close(instance.getZoom(), 2); 412 | deepEqual(instance.getPan(), { x: -300, y: -600 }); 413 | }); 414 | 415 | test("zoom at point by", function() { 416 | expect(2); 417 | instance = initSvgPanZoom({ fit: false }); 418 | 419 | instance.zoomAtPointBy(2, { x: 200, y: 100 }); 420 | 421 | close(instance.getZoom(), 2); 422 | deepEqual(instance.getPan(), { x: -300, y: -600 }); 423 | }); 424 | 425 | test("zoom at point by (with SVG point)", function() { 426 | expect(2); 427 | instance = initSvgPanZoom({ fit: false }); 428 | 429 | var svgPoint = $(svgSelector)[0].createSVGPoint(); 430 | svgPoint.x = 200; 431 | svgPoint.y = 100; 432 | 433 | instance.zoomAtPointBy(2, svgPoint); 434 | 435 | close(instance.getZoom(), 2); 436 | deepEqual(instance.getPan(), { x: -300, y: -600 }); 437 | }); 438 | 439 | test("zoom in", function() { 440 | expect(3); 441 | instance = initSvgPanZoom({ fit: false }); 442 | 443 | instance.zoomIn(); 444 | 445 | close(instance.getZoom(), 1.1); 446 | close(instance.getPan().x, -90); 447 | close(instance.getPan().y, -290); 448 | }); 449 | 450 | test("zoom out", function() { 451 | expect(3); 452 | instance = initSvgPanZoom({ fit: false }); 453 | 454 | instance.zoomOut(); 455 | 456 | close(instance.getZoom(), 0.90909); 457 | close(instance.getPan().x, -13.636374); 458 | close(instance.getPan().y, -213.636374); 459 | }); 460 | 461 | /** 462 | * Zoom settings (min, max, sensitivity) 463 | */ 464 | 465 | test("default min zoom", function() { 466 | expect(1); 467 | // Do not use fit as it will set original zoom different from 1 468 | instance = initSvgPanZoom({ fit: false }); 469 | 470 | instance.zoom(0.1); 471 | 472 | equal(instance.getZoom(), 0.5); 473 | }); 474 | 475 | test("min zoom", function() { 476 | expect(1); 477 | // Do not use fit as it will set original zoom different from 1 478 | instance = initSvgPanZoom({ fit: false, minZoom: 1 }); 479 | 480 | instance.zoom(0.01); 481 | 482 | equal(instance.getZoom(), 1); 483 | }); 484 | 485 | test("default max zoom", function() { 486 | expect(1); 487 | // Do not use fit as it will set original zoom different from 1 488 | instance = initSvgPanZoom({ fit: false }); 489 | 490 | instance.zoom(50); 491 | 492 | equal(instance.getZoom(), 10); 493 | }); 494 | 495 | test("max zoom", function() { 496 | expect(1); 497 | // Do not use fit as it will set original zoom different from 1 498 | instance = initSvgPanZoom({ fit: false, maxZoom: 20 }); 499 | 500 | instance.zoom(50); 501 | 502 | equal(instance.getZoom(), 20); 503 | }); 504 | 505 | test("test zoomScaleSensitivity using zoomIn and zoomOut", function() { 506 | expect(2); 507 | var sensitivity = 0.2; 508 | 509 | // Do not use fit as it will set original zoom different from 1 510 | instance = initSvgPanZoom({ fit: false, zoomScaleSensitivity: sensitivity }); 511 | 512 | // Get initial zoom 513 | var initialZoom = instance.getZoom(); // should be one 514 | 515 | instance.zoomIn(); 516 | 517 | close( 518 | instance.getZoom(), 519 | initialZoom * (1 + sensitivity), 520 | null, 521 | "Check if zoom in uses scale sensitivity right" 522 | ); 523 | 524 | // Lets zoom to 2 525 | instance.zoom(2); 526 | 527 | // Now lets zoom out 528 | instance.zoomOut(); 529 | 530 | close( 531 | instance.getZoom(), 532 | 2 / (1 + sensitivity), 533 | null, 534 | "Check if zoom out uses scale sensitiviry right" 535 | ); 536 | }); 537 | 538 | /** 539 | * Zoom callbacks 540 | */ 541 | 542 | test("before zoom", function() { 543 | expect(1); 544 | instance = initSvgPanZoom(); 545 | 546 | var initialZoom = instance.getZoom(); 547 | 548 | instance.setBeforeZoom(function(scale) { 549 | close(scale, initialZoom); 550 | }); 551 | 552 | instance.zoom(2.3); 553 | 554 | // Remove beforeZoom as it will be called on destroy 555 | instance.setBeforeZoom(null); 556 | 557 | // Zoom one more time to test if it is really removed 558 | instance.zoom(2.4); 559 | }); 560 | 561 | test("on zoom", function() { 562 | expect(1); 563 | instance = initSvgPanZoom(); 564 | 565 | instance.setOnZoom(function(scale) { 566 | close(scale, 2.3); 567 | }); 568 | 569 | instance.zoom(2.3); 570 | 571 | // Remove onZoom as it will be called on destroy 572 | instance.setOnZoom(null); 573 | 574 | // Zoom one more time to test if it is really removed 575 | instance.zoom(2.4); 576 | }); 577 | 578 | /** 579 | * Reseting 580 | */ 581 | 582 | test("reset zoom", function() { 583 | expect(1); 584 | instance = initSvgPanZoom(); 585 | 586 | var initialZoom = instance.getZoom(); 587 | 588 | instance.zoom(2.3); 589 | 590 | instance.resetZoom(); 591 | 592 | close(instance.getZoom(), initialZoom); 593 | }); 594 | 595 | test("reset pan", function() { 596 | expect(1); 597 | instance = initSvgPanZoom(); 598 | 599 | var initialPan = instance.getPan(); 600 | 601 | instance.panBy({ x: 100, y: 300 }); 602 | 603 | instance.resetPan(); 604 | 605 | deepEqual(instance.getPan(), initialPan); 606 | }); 607 | 608 | test("reset (zoom and pan)", function() { 609 | expect(2); 610 | instance = initSvgPanZoom(); 611 | 612 | var initialZoom = instance.getZoom(), 613 | initialPan = instance.getPan(); 614 | 615 | instance.zoom(2.3); 616 | instance.panBy({ x: 100, y: 300 }); 617 | 618 | instance.reset(); 619 | 620 | close(instance.getZoom(), initialZoom); 621 | deepEqual(instance.getPan(), initialPan); 622 | }); 623 | 624 | /** 625 | * Fit and center 626 | */ 627 | 628 | /** 629 | * SVG size 700x300 630 | * viewport zise 800x800 631 | * 632 | * If no viewBox attribute then initial zoom is always 1 633 | */ 634 | test("fit when initialized with fit: true", function() { 635 | expect(1); 636 | instance = initSvgPanZoom(); 637 | 638 | instance.fit(); 639 | 640 | close(instance.getZoom(), 1); 641 | }); 642 | 643 | /** 644 | * SVG size 700x300 645 | * viewport zise 800x800 646 | * zoom = Math.min(700/800, 300/800) = 0.375 647 | */ 648 | test("fit when initialized with fit: false", function() { 649 | expect(1); 650 | instance = initSvgPanZoom({ fit: false, minZoom: 0.1 }); 651 | 652 | instance.fit(); 653 | 654 | close(instance.getZoom(), 0.375); 655 | }); 656 | 657 | /** 658 | * SVG size 700x300 659 | * viewport zise 800x800 (sides ratio is 1) 660 | * zoom 1 => width = height = 300 661 | * 662 | * panX = (700 - 300)/2 = 200 663 | * panY = (300 - 300)/2 = 0 664 | */ 665 | test("center when zoom is 1", function() { 666 | expect(1); 667 | instance = initSvgPanZoom(); 668 | 669 | instance.center(); 670 | 671 | deepEqual(instance.getPan(), { x: 200, y: 0 }); 672 | }); 673 | 674 | /** 675 | * SVG size 700x300 676 | * viewport zise 800x800 (sides ratio is 1) 677 | * zoom 0.5 => width = height = 150 678 | * 679 | * panX = (700 - 150)/2 = 275 680 | * panY = (300 - 150)/2 = 75 681 | */ 682 | test("center when zoom is 0.5", function() { 683 | expect(1); 684 | instance = initSvgPanZoom(); 685 | 686 | instance.zoom(0.5); 687 | instance.center(); 688 | 689 | deepEqual(instance.getPan(), { x: 275, y: 75 }); 690 | }); 691 | 692 | /** 693 | * Resize 694 | */ 695 | 696 | // TODO resize 697 | 698 | /** 699 | * On updated CTM callback 700 | */ 701 | 702 | asyncTest("onUpdatedCTM is called", function() { 703 | // onUpdatedCTM will get called once on init and once after panBy 704 | expect(2); 705 | 706 | instance = initSvgPanZoom(); 707 | instance.setOnUpdatedCTM(function() { 708 | QUnit.ok(true, "onUpdatedCTM got called"); 709 | }); 710 | instance.panBy({ x: 100, y: 300 }); 711 | 712 | setTimeout(function() { 713 | start(); 714 | }, 100); 715 | }); 716 | 717 | /** 718 | * Destroy 719 | */ 720 | 721 | test("after destroy calling svgPanZoom again should return a new instance", function() { 722 | expect(1); 723 | instance = initSvgPanZoom(); 724 | 725 | instance.destroy(); 726 | 727 | var instance2 = initSvgPanZoom(); 728 | 729 | notStrictEqual(instance2, instance); 730 | 731 | // Set it as null so teardown will not try to destroy it again 732 | instance = null; 733 | 734 | // Destroy second instance 735 | instance2.destroy(); 736 | instance2 = null; 737 | }); 738 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![svg-pan-zoom logo](http://bumbu.github.io/svg-pan-zoom/svg-pan-zoom-logo.png) 2 | 3 | svg-pan-zoom library 4 | ========================== 5 | 6 | Simple pan/zoom solution for SVGs in HTML. It adds events listeners for mouse scroll, double-click and pan, plus it optionally offers: 7 | * JavaScript API for control of pan and zoom behavior 8 | * onPan and onZoom event handlers 9 | * On-screen zoom controls 10 | 11 | It works cross-browser and supports both inline SVGs and SVGs in HTML `object` or `embed` elements. 12 | 13 | > If you're looking for version 2.3.x you can find it in [v2.3.x branch](https://github.com/bumbu/svg-pan-zoom/tree/v2.3.x) 14 | 15 | Support 16 | ------- 17 | 18 | ### Bugs and Issues 19 | 20 | If you found a bug or have a suggestion first check if there is a similar [open](https://github.com/bumbu/svg-pan-zoom/issues) or [closed](https://github.com/bumbu/svg-pan-zoom/issues?q=is%3Aissue+is%3Aclosed) issue. If there are none then create a new one. 21 | 22 | When opening a new issue **please provide a reproducible example**: 23 | - Share it so we can get directly to the problem. You can use [this starter jsfiddle setup](http://jsfiddle.net/bumbu/167usffr/) to provide your example. Or upload your own [jsfiddle.net](http://jsfiddle.net) (or any other live) example. 24 | - Mention your library version (located in library file in header) 25 | - Mention your browser name, version and operating system 26 | - Mention any other important for debug details 27 | 28 | ### Solved Bugs and Implemented Features 29 | 30 | If you solved a bug or implemented a feature that may be useful for others then you're welcome to create a pull request. 31 | 32 | ### Questions, How To's, Need Help 33 | 34 | If you have any other type of questions, problems, your code is not working or you want to critique the library - you can use StackOverflow. Just tag your question with [`svgpanzoom`](http://stackoverflow.com/questions/tagged/svgpanzoom). 35 | 36 | ### Contributions/Pull Requests 37 | 38 | Best way to contribute is to create a pull request. In order to create a pull request: 39 | * Fork this repository 40 | * Clone repository fork (created in previous step) locally (on your machine) 41 | * Ensure that you have nodejs and npm installed locally 42 | * In console: 43 | * `cd` into project folder 44 | * Run `npm install` 45 | * Run `npm install -g gulp` if you don't have it already installed globally 46 | * Running `gulp` will listen for source files changes (in `src` folder) and will automatically build distribution files 47 | * Running `gulp compile` will compile source files 48 | * Running `gulp check` will check syntax and automatically fix some errors 49 | * Running `gulp test` will run tests 50 | * Running `gulp build` will prepare the project for a new release 51 | * Implement the change using `gulp` or `gulp compile` 52 | * After change is done test it with `gulp check` and `gulp test` 53 | * Commit only meaningful changes. **Do not commit distribution files (`dist` folder)**. Distribution files are built only before a release 54 | * Push your changes into your fork 55 | * Create a pull request 56 | 57 | Demos 58 | ----- 59 | Pan and zoom the SVG tiger on github pages: 60 | * [Single Inline SVG](http://bumbu.github.io/svg-pan-zoom/demo/inline.html) 61 | * [Multiple Inline SVGs](http://bumbu.github.io/svg-pan-zoom/demo/multi-instance.html) 62 | * [SVG Inserted with `Embed` Element](http://bumbu.github.io/svg-pan-zoom/demo/embed.html) 63 | * [SVG Inserted with `Object` Element](http://bumbu.github.io/svg-pan-zoom/demo/object.html) 64 | * [SVG Inserted with `Img` Element](http://bumbu.github.io/svg-pan-zoom/demo/img.html) (These cannot be panned/zoomed.) 65 | * [SVG With custom controls](http://bumbu.github.io/svg-pan-zoom/demo/custom-controls.html) 66 | * [Resize SVG container on document resize](http://bumbu.github.io/svg-pan-zoom/demo/resize.html) 67 | * [Two SVGs with synchronized zooming and panning](http://bumbu.github.io/svg-pan-zoom/demo/sinchronized.html) 68 | * [Custom events: Touch events support: pan, double tap, pinch](http://bumbu.github.io/svg-pan-zoom/demo/mobile.html) 69 | * [Custom events: Enable zooming only on click, disable on mouse out](http://bumbu.github.io/svg-pan-zoom/demo/custom-event-handlers.html) 70 | * [Limit pan](http://bumbu.github.io/svg-pan-zoom/demo/limit-pan.html) 71 | * [Dynamic SVG load](http://bumbu.github.io/svg-pan-zoom/demo/dynamic-load.html) 72 | * [Using Require.js](http://bumbu.github.io/svg-pan-zoom/demo/require.html) 73 | * [Pan animation](http://bumbu.github.io/svg-pan-zoom/demo/simple-animation.html) 74 | * [Zooming just one SVG layer](http://bumbu.github.io/svg-pan-zoom/demo/layers.html) 75 | * [Thumbnail Viewer](http://bumbu.github.io/svg-pan-zoom/demo/thumbnailViewer.html) 76 | 77 | How To Use 78 | ---------- 79 | 80 | Reference the [svg-pan-zoom.js file](https://github.com/bumbu/svg-pan-zoom/blob/master/dist/svg-pan-zoom.min.js) from your HTML document. Then call the init method: 81 | 82 | ```js 83 | var panZoomTiger = svgPanZoom('#demo-tiger'); 84 | // or 85 | var svgElement = document.querySelector('#demo-tiger') 86 | var panZoomTiger = svgPanZoom(svgElement) 87 | ``` 88 | 89 | First argument to function should be a CSS selector of SVG element or a DOM Element. 90 | 91 | If you want to override the defaults, you can optionally specify one or more arguments: 92 | 93 | ```js 94 | svgPanZoom('#demo-tiger', { 95 | viewportSelector: '.svg-pan-zoom_viewport' 96 | , panEnabled: true 97 | , controlIconsEnabled: false 98 | , zoomEnabled: true 99 | , dblClickZoomEnabled: true 100 | , mouseWheelZoomEnabled: true 101 | , preventMouseEventsDefault: true 102 | , zoomScaleSensitivity: 0.2 103 | , minZoom: 0.5 104 | , maxZoom: 10 105 | , fit: true 106 | , contain: false 107 | , center: true 108 | , refreshRate: 'auto' 109 | , beforeZoom: function(){} 110 | , onZoom: function(){} 111 | , beforePan: function(){} 112 | , onPan: function(){} 113 | , onUpdatedCTM: function(){} 114 | , customEventsHandler: {} 115 | , eventsListenerElement: null 116 | }); 117 | ``` 118 | 119 | If any arguments are specified, they must have the following value types: 120 | * 'viewportSelector' can be querySelector string or SVGElement. 121 | * 'panEnabled' must be true or false. Default is true. 122 | * 'controlIconsEnabled' must be true or false. Default is false. 123 | * 'zoomEnabled' must be true or false. Default is true. 124 | * 'dblClickZoomEnabled' must be true or false. Default is true. 125 | * 'mouseWheelZoomEnabled' must be true or false. Default is true. 126 | * 'preventMouseEventsDefault' must be true or false. Default is true. 127 | * 'zoomScaleSensitivity' must be a scalar. Default is 0.2. 128 | * 'minZoom' must be a scalar. Default is 0.5. 129 | * 'maxZoom' must be a scalar. Default is 10. 130 | * 'fit' must be true or false. Default is true. 131 | * 'contain' must be true or false. Default is false. 132 | * 'center' must be true or false. Default is true. 133 | * 'refreshRate' must be a number or 'auto' 134 | * 'beforeZoom' must be a callback function to be called before zoom changes. 135 | * 'onZoom' must be a callback function to be called when zoom changes. 136 | * 'beforePan' must be a callback function to be called before pan changes. 137 | * 'onPan' must be a callback function to be called when pan changes. 138 | * 'customEventsHandler' must be an object with `init` and `destroy` arguments as functions. 139 | * 'eventsListenerElement' must be an SVGElement or null. 140 | 141 | `beforeZoom` will be called with 2 float attributes: oldZoom and newZoom. 142 | If `beforeZoom` will return `false` then zooming will be halted. 143 | 144 | `onZoom` callbacks will be called with one float attribute representing new zoom scale. 145 | 146 | `beforePan` will be called with 2 attributes: 147 | * `oldPan` 148 | * `newPan` 149 | 150 | Each of these objects has two attributes (x and y) representing current pan (on X and Y axes). 151 | 152 | If `beforePan` will return `false` or an object `{x: true, y: true}` then panning will be halted. 153 | If you want to prevent panning only on one axis then return an object of type `{x: true, y: false}`. 154 | You can alter panning on X and Y axes by providing alternative values through return `{x: 10, y: 20}`. 155 | 156 | > *Caution!* If you alter panning by returning custom values `{x: 10, y: 20}` it will update only current pan step. If panning is done by mouse/touch you have to take in account that next pan step (after the one that you altered) will be performed with values that do not consider altered values (as they even did not existed). 157 | 158 | `onPan` callback will be called with one attribute: `newPan`. 159 | 160 | > *Caution!* Calling zoom or pan API methods from inside of `beforeZoom`, `onZoom`, `beforePan` and `onPan` callbacks may lead to infinite loop. 161 | 162 | `onUpdatedCTM` will get called after the CTM will get updated. That happens asynchronously from pan and zoom events. 163 | 164 | `panEnabled` and `zoomEnabled` are related only to user interaction. If any of this options are disabled - you still can zoom and pan via API. 165 | 166 | `fit` takes precedence over `contain`. So if you set `fit: true` then `contain`'s value doesn't matter. 167 | 168 | Embedding remote files 169 | --------------------- 170 | 171 | If you're embedding a remote file like this 172 | ```html 173 | 174 | Your browser does not support SVG 175 | ``` 176 | 177 | or you're rendering the SVG after the page loads then you'll have to call svgPanZoom library after your SVG is loaded. 178 | 179 | One way to do so is by listening to load event: 180 | ```html 181 | 182 | 183 | 189 | ``` 190 | 191 | 192 | Using a custom viewport 193 | ----------------------- 194 | 195 | You may want to use a custom viewport if you have more layers in your SVG but you want to _pan-zoom_ only one of them. 196 | 197 | By default if: 198 | * There is just one top-level graphical element of type SVGGElement (``) 199 | * SVGGElement has no `transform` attribute 200 | * There is no other SVGGElement with class name `svg-pan-zoom_viewport` 201 | 202 | then the top-level graphical element will be used as viewport. 203 | 204 | To specify which layer (SVGGElement) should be _pan-zoomed_ set the `svg-pan-zoom_viewport` class name to that element: 205 | ``. 206 | 207 | > Do not set any _transform_ attributes to that element. It will make the library misbehave. 208 | > If you need _transform_ attribute for viewport better create a nested group element and set _transforms_ to that element: 209 | ```html 210 | 211 | 212 | 213 | ``` 214 | 215 | You can specify your own viewport selector by altering `viewportSelector` config value: 216 | ```js 217 | svgPanZoom('#demo-tiger', { 218 | viewportSelector: '.svg-pan-zoom_viewport' 219 | }); 220 | // or 221 | var viewportGroupElement = document.getElementById('demo-tiger').querySelector('.svg-pan-zoom_viewport'); 222 | svgPanZoom('#demo-tiger', { 223 | viewportSelector: viewportGroupElement 224 | }); 225 | ``` 226 | 227 | Listening for pan/zoom events on a child SVG element 228 | ---------------------------------------------------- 229 | 230 | If you want to listen for user interaction events from a child SVG element then use `eventsListenerElement` option. An example is available in [demo/layers.html](http://bumbu.github.io/svg-pan-zoom/demo/layers.html). 231 | 232 | Use with browserify 233 | ------------------- 234 | 235 | To use with browserify, follow these steps: 236 | * Add the package as node module `npm install --save bumbu/svg-pan-zoom` 237 | * Require _svg-pan-zoom_ in your source file `svgPanZoom = require('svg-pan-zoom')` 238 | * Use in the same way as you would do with global svgPanZoom: `instance = svgPanZoom('#demo-tiger')` 239 | 240 | Use with Require.js (or other AMD libraries) 241 | ------------------- 242 | 243 | An example of how to load library using Require.js is available in [demo/require.html](http://bumbu.github.io/svg-pan-zoom/demo/require.html) 244 | 245 | Custom events support 246 | --------------------- 247 | 248 | You may want to add custom events support (for example double tap or pinch). 249 | 250 | It is possible by setting `customEventsHandler` configuration option. 251 | `customEventsHandler` should be an object with following attributes: 252 | * `haltEventListeners`: array of strings 253 | * `init`: function 254 | * `destroy`: function 255 | 256 | `haltEventListeners` specifies which default event listeners should be disabled (in order to avoid conflicts as svg-pan-zoom by default supports panning using touch events). 257 | 258 | `init` is a function that is called when svg-pan-zoom is initialized. An object is passed into this function. 259 | Passed object has following attributes: 260 | * `svgElement` - SVGSVGElement 261 | * `instance` - svg-pan-zoom public API instance 262 | 263 | `destroy` is a function called upon svg-pan-zoom destroy 264 | 265 | An example of how to use it together with [Hammer.js](http://hammerjs.github.io): 266 | ```js 267 | var options = { 268 | zoomEnabled: true 269 | , controlIconsEnabled: true 270 | , customEventsHandler: { 271 | // Halt all touch events 272 | haltEventListeners: ['touchstart', 'touchend', 'touchmove', 'touchleave', 'touchcancel'] 273 | 274 | // Init custom events handler 275 | , init: function(options) { 276 | // Init Hammer 277 | this.hammer = Hammer(options.svgElement) 278 | 279 | // Handle double tap 280 | this.hammer.on('doubletap', function(ev){ 281 | options.instance.zoomIn() 282 | }) 283 | } 284 | 285 | // Destroy custom events handler 286 | , destroy: function(){ 287 | this.hammer.destroy() 288 | } 289 | } 290 | } 291 | 292 | svgPanZoom('#mobile-svg', options); 293 | ``` 294 | 295 | You may find an example that adds support for Hammer.js pan, pinch and doubletap in demo/mobile.html 296 | 297 | Keep content visible/Limit pan 298 | ------------------------------ 299 | 300 | You may want to keep SVG content visible by not allowing panning over SVG borders. 301 | 302 | To do so you may prevent or alter panning from `beforePan` callback. For more details take a look at `demo/limit-pan.html` example. 303 | 304 | Public API 305 | ---------- 306 | 307 | When you call `svgPanZoom` method it returns an object with following methods: 308 | * enablePan 309 | * disablePan 310 | * isPanEnabled 311 | * pan 312 | * panBy 313 | * getPan 314 | * setBeforePan 315 | * setOnPan 316 | * enableZoom 317 | * disableZoom 318 | * isZoomEnabled 319 | * enableControlIcons 320 | * disableControlIcons 321 | * isControlIconsEnabled 322 | * enableDblClickZoom 323 | * disableDblClickZoom 324 | * isDblClickZoomEnabled 325 | * enableMouseWheelZoom 326 | * disableMouseWheelZoom 327 | * isMouseWheelZoomEnabled 328 | * setZoomScaleSensitivity 329 | * setMinZoom 330 | * setMaxZoom 331 | * setBeforeZoom 332 | * setOnZoom 333 | * zoom 334 | * zoomBy 335 | * zoomAtPoint 336 | * zoomAtPointBy 337 | * zoomIn 338 | * zoomOut 339 | * setOnUpdatedCTM 340 | * getZoom 341 | * resetZoom 342 | * resetPan 343 | * reset 344 | * fit 345 | * contain 346 | * center 347 | * updateBBox 348 | * resize 349 | * getSizes 350 | * destroy 351 | 352 | To programmatically pan, call the pan method with vector as first argument: 353 | 354 | ```js 355 | // Get instance 356 | var panZoomTiger = svgPanZoom('#demo-tiger'); 357 | 358 | // Pan to rendered point x = 50, y = 50 359 | panZoomTiger.pan({x: 50, y: 50}) 360 | 361 | // Pan by x = 50, y = 50 of rendered pixels 362 | panZoomTiger.panBy({x: 50, y: 50}) 363 | ``` 364 | 365 | To programmatically zoom, you can use the zoom method to specify your desired scale value: 366 | 367 | ```js 368 | // Get instance 369 | var panZoomTiger = svgPanZoom('#demo-tiger'); 370 | 371 | // Set zoom level to 2 372 | panZoomTiger.zoom(2) 373 | 374 | // Zoom by 130% 375 | panZoomTiger.zoomBy(1.3) 376 | 377 | // Set zoom level to 2 at point 378 | panZoomTiger.zoomAtPoint(2, {x: 50, y: 50}) 379 | 380 | // Zoom by 130% at given point 381 | panZoomTiger.zoomAtPointBy(1.3, {x: 50, y: 50}) 382 | ``` 383 | 384 | > Zoom is relative to initial SVG internal zoom level. If your SVG was fit at the beginning (option `fit: true`) and thus zoomed in or out to fit available space - initial scale will be 1 anyway. 385 | 386 | Or you can use the zoomIn or zoomOut methods: 387 | 388 | ```js 389 | // Get instance 390 | var panZoomTiger = svgPanZoom('#demo-tiger'); 391 | 392 | panZoomTiger.zoomIn() 393 | panZoomTiger.zoomOut() 394 | panZoomTiger.resetZoom() 395 | ``` 396 | 397 | If you want faster or slower zooming, you can override the default zoom increment with the setZoomScaleSensitivity method. 398 | 399 | To programmatically enable/disable pan or zoom: 400 | 401 | ```js 402 | // Get instance 403 | var panZoomTiger = svgPanZoom('#demo-tiger'); 404 | 405 | panZoomTiger.enablePan(); 406 | panZoomTiger.disablePan(); 407 | 408 | panZoomTiger.enableZoom(); 409 | panZoomTiger.disableZoom(); 410 | ``` 411 | 412 | To fit and center (you may try `contain` instead of `fit`): 413 | 414 | ```js 415 | // Get instance 416 | var panZoomTiger = svgPanZoom('#demo-tiger'); 417 | 418 | panZoomTiger.fit(); 419 | panZoomTiger.center(); 420 | ``` 421 | 422 | If you want to fit and center your SVG after its container resize: 423 | 424 | ```js 425 | // Get instance 426 | var panZoomTiger = svgPanZoom('#demo-tiger'); 427 | 428 | panZoomTiger.resize(); // update SVG cached size and controls positions 429 | panZoomTiger.fit(); 430 | panZoomTiger.center(); 431 | ``` 432 | 433 | If you update SVG (viewport) contents so its border box (virtual box that contains all elements) changes, you have to call `updateBBox`: 434 | 435 | ```js 436 | var panZoomTiger = svgPanZoom('#demo-tiger'); 437 | panZoomTiger.fit(); 438 | 439 | // Update SVG rectangle width 440 | document.getElementById('demo-tiger').querySelector('rect').setAttribute('width', 200) 441 | 442 | // fit does not work right anymore as viewport bounding box changed 443 | panZoomTiger.fit(); 444 | 445 | panZoomTiger.updateBBox(); // Update viewport bounding box 446 | panZoomTiger.fit(); // fit works as expected 447 | ``` 448 | 449 | If you need more data about SVG you can call `getSizes`. It will return an object that will contain: 450 | * `width` - SVG cached width 451 | * `height` - SVG cached height 452 | * `realZoom` - _a_ and _d_ attributes of transform matrix applied over viewport 453 | * `viewBox` - an object containing cached sizes of viewport boxder box 454 | * `width` 455 | * `height` 456 | * `x` - x offset 457 | * `y` - y offset 458 | 459 | Destroy SvgPanZoom instance: 460 | 461 | ```js 462 | // Get instance 463 | var panZoomTiger = svgPanZoom('#demo-tiger'); 464 | 465 | panZoomTiger.destroy(); 466 | delete panZoomTiger; 467 | ``` 468 | 469 | How to test 470 | ----------- 471 | 472 | Before committing you should check your code style by running `gulp check`. 473 | 474 | If you made a change then first build the library. Open `./tests/index.html` in your browser. All tests should pass. 475 | 476 | If you have PhantomJS installed then you can run `gulp test`. 477 | 478 | Common Issues & FAQ 479 | ------------------- 480 | 481 | ### SVG height is broken 482 | 483 | Because the library removes `viewBox` attribute from the SVG element - you may experience that the height of your SVG changed (usually to 150px). In order to fix that you have to add height to the `SVG` or `object`/`embed`. 484 | 485 | ### Calling library methods throw errors when SVG is hidden 486 | 487 | This library does not support working with SVGs that are hidden as some browsers detach child documents from the DOM when those are hidden. See [#279](https://github.com/bumbu/svg-pan-zoom/issues/279) for more details. 488 | 489 | ### Performance issues on initialization 490 | 491 | If performance is bad only on initialization of the library, then consider wrapping all SVG's child elements into a `` beforehand. This way the library will not have to create it and move all children into it (which is the root cause of the issue). See [#146 comment](https://github.com/bumbu/svg-pan-zoom/issues/146#issuecomment-137873358). 492 | 493 | ### Performance issues while panning/zooming 494 | 495 | Most often those are caused by big SVG files. And in those cases it's browsers not being able to handle those SVGs fast enough. 496 | See [#277](https://github.com/bumbu/svg-pan-zoom/issues/277) for more details. 497 | 498 | ### How to limit zooming/panning 499 | 500 | For zooming there is `minZoom` and `maxZoom` zoom config options. 501 | 502 | For panning and custom zoom experiences take a look at [limit-pan example](http://bumbu.github.io/svg-pan-zoom/demo/limit-pan.html). 503 | 504 | ### How to add animations 505 | 506 | Currently there're 2 ways of doing animation: via CSS or programatically - see #101 for more details. 507 | 508 | ### Errors for `object`/`embed` or dynamically loaded SVGs 509 | 510 | You have to ensure that the SVG is loaded and available in DOM before initializing the library. 511 | 512 | Check [dymanic-load demo](http://bumbu.github.io/svg-pan-zoom/demo/dynamic-load.html). 513 | 514 | Supported Browsers 515 | ------------------ 516 | * Chrome 517 | * Firefox 518 | * Safari 519 | * Opera 520 | * Internet Explorer 9+ _(works badly if viewBox attribute is set)_ 521 | 522 | CDN 523 | --- 524 | You can use [jsdelivr](http://www.jsdelivr.com/) as a CDN. It supports automatic pulling from NPM and GitHub. 525 | 526 | For example use `https://cdn.jsdelivr.net/npm/svg-pan-zoom@3.5.0/dist/svg-pan-zoom.min.js` to load version `3.5.0`. 527 | 528 | For more usage examples check [jsdelivr usage](https://github.com/jsdelivr/jsdelivr#usage). 529 | 530 | Related Work 531 | ------------ 532 | This library used the [SVGPan](https://github.com/aleofreddi/svgpan) library as a starting point. SVGPan is intended for use with the [SVG 'script' element](http://www.w3.org/TR/SVG/script.html), whereas svg-pan-zoom is intended for use with the [HTML 'script' element](http://www.w3.org/TR/html401/interact/scripts.html). 533 | 534 | Wrapper Libraries (feel free to add to this -- pull requests welcome!) 535 | * [AngularJS 1.x](https://www.npmjs.com/package/angular-svg-pan-zoom) 536 | * [Vue.js component](https://github.com/yanick/vue-svg-pan-zoom) 537 | * [Meteor](https://github.com/jossoco/meteor-svg-pan-zoom/) 538 | * [R](https://github.com/timelyportfolio/svgPanZoom) 539 | * [Ruby](https://rubygems.org/gems/svg_pan_zoom) 540 | 541 | License 542 | ------- 543 | The code from the SVGPan library is licensed under the following BSD license: 544 | 545 | ``` 546 | Copyright 2009-2010 Andrea Leofreddi . All rights reserved. 547 | 548 | Redistribution and use in source and binary forms, with or without modification, are 549 | permitted provided that the following conditions are met: 550 | 551 | 1. Redistributions of source code must retain the above copyright notice, this list of 552 | conditions and the following disclaimer. 553 | 2. Redistributions in binary form must reproduce the above copyright notice, this list 554 | of conditions and the following disclaimer in the documentation and/or other materials 555 | provided with the distribution. 556 | 557 | THIS SOFTWARE IS PROVIDED BY Andrea Leofreddi "AS IS" AND ANY EXPRESS OR IMPLIED 558 | WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 559 | FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Andrea Leofreddi OR 560 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 561 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 562 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 563 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 564 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 565 | ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 566 | 567 | * The views and conclusions contained in the software and documentation are those of the 568 | authors and should not be interpreted as representing official policies, either expressed 569 | or implied, of Andrea Leofreddi. 570 | ``` 571 | 572 | The code from the updates and changes to SVGPan are licensed under the same BSD license, with the copyright for the code from each change held by the author of that code. 573 | -------------------------------------------------------------------------------- /dist/svg-pan-zoom.min.js: -------------------------------------------------------------------------------- 1 | // svg-pan-zoom v3.6.2 2 | // https://github.com/bumbu/svg-pan-zoom 3 | !function s(r,a,l){function u(e,t){if(!a[e]){if(!r[e]){var o="function"==typeof require&&require;if(!t&&o)return o(e,!0);if(h)return h(e,!0);var n=new Error("Cannot find module '"+e+"'");throw n.code="MODULE_NOT_FOUND",n}var i=a[e]={exports:{}};r[e][0].call(i.exports,function(t){return u(r[e][1][t]||t)},i,i.exports,s,r,a,l)}return a[e].exports}for(var h="function"==typeof require&&require,t=0;tthis.options.maxZoom*n.zoom&&(t=this.options.maxZoom*n.zoom/this.getZoom());var i=this.viewport.getCTM(),s=e.matrixTransform(i.inverse()),r=this.svg.createSVGMatrix().translate(s.x,s.y).scale(t).translate(-s.x,-s.y),a=i.multiply(r);a.a!==i.a&&this.viewport.setCTM(a)},i.prototype.zoom=function(t,e){this.zoomAtPoint(t,a.getSvgCenterPoint(this.svg,this.width,this.height),e)},i.prototype.publicZoom=function(t,e){e&&(t=this.computeFromRelativeZoom(t)),this.zoom(t,e)},i.prototype.publicZoomAtPoint=function(t,e,o){if(o&&(t=this.computeFromRelativeZoom(t)),"SVGPoint"!==r.getType(e)){if(!("x"in e&&"y"in e))throw new Error("Given point is invalid");e=a.createSVGPoint(this.svg,e.x,e.y)}this.zoomAtPoint(t,e,o)},i.prototype.getZoom=function(){return this.viewport.getZoom()},i.prototype.getRelativeZoom=function(){return this.viewport.getRelativeZoom()},i.prototype.computeFromRelativeZoom=function(t){return t*this.viewport.getOriginalState().zoom},i.prototype.resetZoom=function(){var t=this.viewport.getOriginalState();this.zoom(t.zoom,!0)},i.prototype.resetPan=function(){this.pan(this.viewport.getOriginalState())},i.prototype.reset=function(){this.resetZoom(),this.resetPan()},i.prototype.handleDblClick=function(t){var e;if((this.options.preventMouseEventsDefault&&(t.preventDefault?t.preventDefault():t.returnValue=!1),this.options.controlIconsEnabled)&&-1<(t.target.getAttribute("class")||"").indexOf("svg-pan-zoom-control"))return!1;e=t.shiftKey?1/(2*(1+this.options.zoomScaleSensitivity)):2*(1+this.options.zoomScaleSensitivity);var o=a.getEventPoint(t,this.svg).matrixTransform(this.svg.getScreenCTM().inverse());this.zoomAtPoint(e,o)},i.prototype.handleMouseDown=function(t,e){this.options.preventMouseEventsDefault&&(t.preventDefault?t.preventDefault():t.returnValue=!1),r.mouseAndTouchNormalize(t,this.svg),this.options.dblClickZoomEnabled&&r.isDblClick(t,e)?this.handleDblClick(t):(this.state="pan",this.firstEventCTM=this.viewport.getCTM(),this.stateOrigin=a.getEventPoint(t,this.svg).matrixTransform(this.firstEventCTM.inverse()))},i.prototype.handleMouseMove=function(t){if(this.options.preventMouseEventsDefault&&(t.preventDefault?t.preventDefault():t.returnValue=!1),"pan"===this.state&&this.options.panEnabled){var e=a.getEventPoint(t,this.svg).matrixTransform(this.firstEventCTM.inverse()),o=this.firstEventCTM.translate(e.x-this.stateOrigin.x,e.y-this.stateOrigin.y);this.viewport.setCTM(o)}},i.prototype.handleMouseUp=function(t){this.options.preventMouseEventsDefault&&(t.preventDefault?t.preventDefault():t.returnValue=!1),"pan"===this.state&&(this.state="none")},i.prototype.fit=function(){var t=this.viewport.getViewBox(),e=Math.min(this.width/t.width,this.height/t.height);this.zoom(e,!0)},i.prototype.contain=function(){var t=this.viewport.getViewBox(),e=Math.max(this.width/t.width,this.height/t.height);this.zoom(e,!0)},i.prototype.center=function(){var t=this.viewport.getViewBox(),e=.5*(this.width-(t.width+2*t.x)*this.getZoom()),o=.5*(this.height-(t.height+2*t.y)*this.getZoom());this.getPublicInstance().pan({x:e,y:o})},i.prototype.updateBBox=function(){this.viewport.simpleViewBoxCache()},i.prototype.pan=function(t){var e=this.viewport.getCTM();e.e=t.x,e.f=t.y,this.viewport.setCTM(e)},i.prototype.panBy=function(t){var e=this.viewport.getCTM();e.e+=t.x,e.f+=t.y,this.viewport.setCTM(e)},i.prototype.getPan=function(){var t=this.viewport.getState();return{x:t.x,y:t.y}},i.prototype.resize=function(){var t=a.getBoundingClientRectNormalized(this.svg);this.width=t.width,this.height=t.height;var e=this.viewport;e.options.width=this.width,e.options.height=this.height,e.processCTM(),this.options.controlIconsEnabled&&(this.getPublicInstance().disableControlIcons(),this.getPublicInstance().enableControlIcons())},i.prototype.destroy=function(){var e=this;for(var t in this.beforeZoom=null,this.onZoom=null,this.beforePan=null,this.onPan=null,(this.onUpdatedCTM=null)!=this.options.customEventsHandler&&this.options.customEventsHandler.destroy({svgElement:this.svg,eventsListenerElement:this.options.eventsListenerElement,instance:this.getPublicInstance()}),this.eventListeners)(this.options.eventsListenerElement||this.svg).removeEventListener(t,this.eventListeners[t],!this.options.preventMouseEventsDefault&&h);this.disableMouseWheelZoom(),this.getPublicInstance().disableControlIcons(),this.reset(),c=c.filter(function(t){return t.svg!==e.svg}),delete this.options,delete this.viewport,delete this.publicInstance,delete this.pi,this.getPublicInstance=function(){return null}},i.prototype.getPublicInstance=function(){var o=this;return this.publicInstance||(this.publicInstance=this.pi={enablePan:function(){return o.options.panEnabled=!0,o.pi},disablePan:function(){return o.options.panEnabled=!1,o.pi},isPanEnabled:function(){return!!o.options.panEnabled},pan:function(t){return o.pan(t),o.pi},panBy:function(t){return o.panBy(t),o.pi},getPan:function(){return o.getPan()},setBeforePan:function(t){return o.options.beforePan=null===t?null:r.proxy(t,o.publicInstance),o.pi},setOnPan:function(t){return o.options.onPan=null===t?null:r.proxy(t,o.publicInstance),o.pi},enableZoom:function(){return o.options.zoomEnabled=!0,o.pi},disableZoom:function(){return o.options.zoomEnabled=!1,o.pi},isZoomEnabled:function(){return!!o.options.zoomEnabled},enableControlIcons:function(){return o.options.controlIconsEnabled||(o.options.controlIconsEnabled=!0,s.enable(o)),o.pi},disableControlIcons:function(){return o.options.controlIconsEnabled&&(o.options.controlIconsEnabled=!1,s.disable(o)),o.pi},isControlIconsEnabled:function(){return!!o.options.controlIconsEnabled},enableDblClickZoom:function(){return o.options.dblClickZoomEnabled=!0,o.pi},disableDblClickZoom:function(){return o.options.dblClickZoomEnabled=!1,o.pi},isDblClickZoomEnabled:function(){return!!o.options.dblClickZoomEnabled},enableMouseWheelZoom:function(){return o.enableMouseWheelZoom(),o.pi},disableMouseWheelZoom:function(){return o.disableMouseWheelZoom(),o.pi},isMouseWheelZoomEnabled:function(){return!!o.options.mouseWheelZoomEnabled},setZoomScaleSensitivity:function(t){return o.options.zoomScaleSensitivity=t,o.pi},setMinZoom:function(t){return o.options.minZoom=t,o.pi},setMaxZoom:function(t){return o.options.maxZoom=t,o.pi},setBeforeZoom:function(t){return o.options.beforeZoom=null===t?null:r.proxy(t,o.publicInstance),o.pi},setOnZoom:function(t){return o.options.onZoom=null===t?null:r.proxy(t,o.publicInstance),o.pi},zoom:function(t){return o.publicZoom(t,!0),o.pi},zoomBy:function(t){return o.publicZoom(t,!1),o.pi},zoomAtPoint:function(t,e){return o.publicZoomAtPoint(t,e,!0),o.pi},zoomAtPointBy:function(t,e){return o.publicZoomAtPoint(t,e,!1),o.pi},zoomIn:function(){return this.zoomBy(1+o.options.zoomScaleSensitivity),o.pi},zoomOut:function(){return this.zoomBy(1/(1+o.options.zoomScaleSensitivity)),o.pi},getZoom:function(){return o.getRelativeZoom()},setOnUpdatedCTM:function(t){return o.options.onUpdatedCTM=null===t?null:r.proxy(t,o.publicInstance),o.pi},resetZoom:function(){return o.resetZoom(),o.pi},resetPan:function(){return o.resetPan(),o.pi},reset:function(){return o.reset(),o.pi},fit:function(){return o.fit(),o.pi},contain:function(){return o.contain(),o.pi},center:function(){return o.center(),o.pi},updateBBox:function(){return o.updateBBox(),o.pi},resize:function(){return o.resize(),o.pi},getSizes:function(){return{width:o.width,height:o.height,realZoom:o.getZoom(),viewBox:o.viewport.getViewBox()}},destroy:function(){return o.destroy(),o.pi}}),this.publicInstance};var c=[];e.exports=function(t,e){var o=r.getSvg(t);if(null===o)return null;for(var n=c.length-1;0<=n;n--)if(c[n].svg===o)return c[n].instance.getPublicInstance();return c.push({svg:o,instance:new i(o,e)}),c[c.length-1].instance.getPublicInstance()}},{"./control-icons":1,"./shadow-viewport":2,"./svg-utilities":5,"./uniwheel":6,"./utilities":7}],5:[function(t,e,o){var l=t("./utilities"),s="unknown";document.documentMode&&(s="ie"),e.exports={svgNS:"http://www.w3.org/2000/svg",xmlNS:"http://www.w3.org/XML/1998/namespace",xmlnsNS:"http://www.w3.org/2000/xmlns/",xlinkNS:"http://www.w3.org/1999/xlink",evNS:"http://www.w3.org/2001/xml-events",getBoundingClientRectNormalized:function(t){if(t.clientWidth&&t.clientHeight)return{width:t.clientWidth,height:t.clientHeight};if(t.getBoundingClientRect())return t.getBoundingClientRect();throw new Error("Cannot get BoundingClientRect for SVG.")},getOrCreateViewport:function(t,e){var o=null;if(!(o=l.isElement(e)?e:t.querySelector(e))){var n=Array.prototype.slice.call(t.childNodes||t.children).filter(function(t){return"defs"!==t.nodeName&&"#text"!==t.nodeName});1===n.length&&"g"===n[0].nodeName&&null===n[0].getAttribute("transform")&&(o=n[0])}if(!o){var i="viewport-"+(new Date).toISOString().replace(/\D/g,"");(o=document.createElementNS(this.svgNS,"g")).setAttribute("id",i);var s=t.childNodes||t.children;if(s&&0