├── .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 |
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 | Zoom in
13 | Zoom out
14 | Reset
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 | swap
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 |
Animate
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 | '' +
234 | escapeHtml(fileName) +
235 | " "
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 | 
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