├── .babelrc ├── .eslintignore ├── .eslintrc.json ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── codeql-analysis.yml ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── assets └── img │ ├── journey.jpg │ ├── journey_start.jpg │ ├── journey_start_thumbnail.jpg │ └── journey_thumbnail.jpg ├── build ├── zooming.js ├── zooming.min.js └── zooming.module.js ├── cypress.json ├── cypress ├── fixtures │ └── example.json ├── integration │ └── demo_spec.js ├── plugins │ └── index.js └── support │ ├── commands.js │ └── index.js ├── docs ├── .nojekyll ├── README.md ├── api-reference.md ├── configuration.md ├── guide.md ├── index.html └── sidebar.md ├── examples ├── grid │ └── index.html └── image-size │ └── index.html ├── index.html ├── package.json ├── rollup.config.js ├── rollup.config.min.js ├── src ├── handler.js ├── index.js ├── options.js ├── overlay.js ├── target.js └── utils.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015-rollup" 4 | ], 5 | "plugins": [ 6 | "transform-object-assign" 7 | ] 8 | } -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | demo/js/* 2 | docs/assets/* 3 | test/* 4 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "node": true, 5 | "es6": true, 6 | "mocha": true, 7 | "jasmine": true 8 | }, 9 | "extends": "eslint:recommended", 10 | "parserOptions": { 11 | "ecmaVersion": 6, 12 | "sourceType": "module" 13 | }, 14 | "rules": { 15 | "indent": [ 16 | "off", 17 | 2 18 | ], 19 | "linebreak-style": [ 20 | "error", 21 | "unix" 22 | ], 23 | "quotes": [ 24 | "error", 25 | "single" 26 | ], 27 | "semi": [ 28 | "error", 29 | "never" 30 | ] 31 | }, 32 | "globals": { 33 | "cy": false 34 | } 35 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | Make sure the bug can be reproduced in either [demo](https://kingdido999.github.io/zooming/) page or a forked [codepen](https://codepen.io/kingdido999/pen/rpYrKV) page. 11 | 12 | **Describe the bug** 13 | A clear and concise description of what the bug is. 14 | 15 | **To Reproduce** 16 | Steps to reproduce the behavior: 17 | 1. 18 | 19 | **Expected behavior** 20 | A clear and concise description of what you expected to happen. 21 | 22 | **Desktop (please complete the following information):** 23 | - OS: [e.g. iOS] 24 | - Browser [e.g. chrome, safari] 25 | - Version [e.g. 22] 26 | 27 | **Smartphone (please complete the following information):** 28 | - Device: [e.g. iPhone6] 29 | - OS: [e.g. iOS8.1] 30 | - Browser [e.g. stock browser, safari] 31 | - Version [e.g. 22] 32 | 33 | **Additional context** 34 | Add any other context about the problem here. 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: feature 6 | assignees: '' 7 | 8 | --- 9 | 10 | Before you submit a feature request, please make sure to read through [Documentation](https://kingdido999.github.io/zooming/docs). 11 | 12 | **Is your feature request related to a problem? Please describe.** 13 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 14 | 15 | **Describe the solution you'd like** 16 | A clear and concise description of what you want to happen. 17 | 18 | **Describe alternatives you've considered** 19 | A clear and concise description of any alternative solutions or features you've considered. 20 | 21 | **Additional context** 22 | Add any other context or screenshots about the feature request here. 23 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | name: "CodeQL" 7 | 8 | on: 9 | push: 10 | branches: [master] 11 | pull_request: 12 | # The branches below must be a subset of the branches above 13 | branches: [master] 14 | schedule: 15 | - cron: '0 0 * * 6' 16 | 17 | jobs: 18 | analyze: 19 | name: Analyze 20 | runs-on: ubuntu-latest 21 | 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | # Override automatic language detection by changing the below list 26 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] 27 | language: ['javascript'] 28 | # Learn more... 29 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection 30 | 31 | steps: 32 | - name: Checkout repository 33 | uses: actions/checkout@v2 34 | 35 | # Initializes the CodeQL tools for scanning. 36 | - name: Initialize CodeQL 37 | uses: github/codeql-action/init@v1 38 | with: 39 | languages: ${{ matrix.language }} 40 | # If you wish to specify custom queries, you can do so here or in a config file. 41 | # By default, queries listed here will override any specified in a config file. 42 | # Prefix the list here with "+" to use these queries and those in the config file. 43 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 44 | 45 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 46 | # If this step fails, then you should remove it and run the build manually (see below) 47 | - name: Autobuild 48 | uses: github/codeql-action/autobuild@v1 49 | 50 | # ℹ️ Command-line programs to run using the OS shell. 51 | # 📚 https://git.io/JvXDl 52 | 53 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 54 | # and modify them (or add more) to build your code if your project 55 | # uses a compiled language 56 | 57 | #- run: | 58 | # make bootstrap 59 | # make release 60 | 61 | - name: Perform CodeQL Analysis 62 | uses: github/codeql-action/analyze@v1 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | npm-debug.log 3 | node_modules/ 4 | yarn-error.log 5 | cypress/videos/ -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .babelrc 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | cache: 5 | directories: 6 | - "node_modules" 7 | before_script: 8 | yarn run build 9 | script: 10 | yarn test 11 | addons: 12 | chrome: stable -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-present Desmond Ding 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Zooming [![npm](https://img.shields.io/npm/v/zooming.svg?style=flat-square)](https://www.npmjs.com/package/zooming) [![npm bundle size](https://img.shields.io/bundlephobia/minzip/zooming.svg?style=flat-square)](https://bundlephobia.com/result?p=zooming) 2 | 3 | Image zoom that makes sense. 4 | 5 | - Pure JavaScript & built with mobile in mind. 6 | - Smooth animations with intuitive gestures. 7 | - Zoom into a hi-res image if supplied. 8 | - Easy to integrate & customizable. 9 | 10 | ## Get Started 11 | 12 | Try [Demo](https://kingdido999.github.io/zooming/) or play with [codepen](https://codepen.io/kingdido999/pen/rpYrKV). 13 | 14 | Please see [Documentation](https://kingdido999.github.io/zooming/docs) for detailed guide. 15 | 16 | ## Showcase 17 | 18 | These projects are using Zooming. Pull requests are welcome! 19 | 20 | - [beta](https://github.com/sunya9/beta): pnut.io web client. 21 | - [bluedoc](https://github.com/thebluedoc/bluedoc): an open-source document management tool for enterprise self host. 22 | - [Chalk](https://github.com/nielsenramon/chalk): a high quality, completely customizable, performant and 100% free Jekyll blog theme. 23 | - [Drupal Zooming](https://www.drupal.org/project/zooming): integrate Zooming to Drupal. 24 | - [imagediff](https://github.com/Showmax/imagediff): tool for automated UI testing and catching visual regressions. 25 | - [OctoberCMS Zooming Images plugin](https://github.com/alex-lit/OctoberCMS-Zooming-Images-Plugin): open source plugin for October CMS. 26 | - [vuepress-plugin-zooming](https://github.com/vuepress/vuepress-plugin-zooming): make images zoomable in VuePress. 27 | - [docusaurus-plugin-zooming](https://github.com/inovector/docusaurus-plugin-zooming): make images zoomable in Docusaurus. 28 | 29 | ## Caveats / Limitations 30 | 31 | - Avoid working with fixed position images [#34](https://github.com/kingdido999/zooming/issues/34). 32 | - Image won't be visible after zoom-in if any parent element has style `overflow: hidden` [#22](https://github.com/kingdido999/zooming/issues/22). 33 | 34 | ## Contributing 35 | 36 | Fork it. Under project folder: 37 | 38 | ```bash 39 | yarn 40 | yarn start 41 | ``` 42 | 43 | Open up `index.html` in browser. 44 | 45 | Make your changes and submit a pull request! 46 | 47 | ## Test 48 | 49 | `yarn test` 50 | 51 | ## Credit 52 | 53 | Inspired by [zoom.js](https://github.com/fat/zoom.js) and [zoomerang](https://github.com/yyx990803/zoomerang). First demo image from [Journey](http://thatgamecompany.com/games/journey/). Second demo image [journey](http://www.pixiv.net/member_illust.php?mode=medium&illust_id=36017129) by [飴村](http://www.pixiv.net/member.php?id=47488). 54 | -------------------------------------------------------------------------------- /assets/img/journey.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingdido999/zooming/3d813c2f27bdebaeada3e34cd8ae3790b4b83d95/assets/img/journey.jpg -------------------------------------------------------------------------------- /assets/img/journey_start.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingdido999/zooming/3d813c2f27bdebaeada3e34cd8ae3790b4b83d95/assets/img/journey_start.jpg -------------------------------------------------------------------------------- /assets/img/journey_start_thumbnail.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingdido999/zooming/3d813c2f27bdebaeada3e34cd8ae3790b4b83d95/assets/img/journey_start_thumbnail.jpg -------------------------------------------------------------------------------- /assets/img/journey_thumbnail.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingdido999/zooming/3d813c2f27bdebaeada3e34cd8ae3790b4b83d95/assets/img/journey_thumbnail.jpg -------------------------------------------------------------------------------- /build/zooming.min.js: -------------------------------------------------------------------------------- 1 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t=t||self).Zooming=e()}(this,function(){"use strict";var t="auto",n="zoom-in",o="zoom-out",r="grab",a="move";function l(t,e,i){var n={passive:!1};!(3=o||Math.abs(n)>=o)&&(this.lastScrollPosition=null,this.close())},keydown:function(t){var e;("Escape"===((e=t).key||e.code)||27===e.keyCode)&&(this.released?this.close():this.release(this.close))},mousedown:function(t){if(d(t)&&!f(t)){t.preventDefault();var e=t.clientX,i=t.clientY;this.pressTimer=setTimeout(function(){this.grab(e,i)}.bind(this),200)}},mousemove:function(t){this.released||this.move(t.clientX,t.clientY)},mouseup:function(t){d(t)&&!f(t)&&(clearTimeout(this.pressTimer),this.released?this.close():this.release())},touchstart:function(t){t.preventDefault();var e=t.touches[0],i=e.clientX,n=e.clientY;this.pressTimer=setTimeout(function(){this.grab(i,n)}.bind(this),200)},touchmove:function(t){if(!this.released){var e=t.touches[0],i=e.clientX,n=e.clientY;this.move(i,n)}},touchend:function(t){void t.targetTouches.length||(clearTimeout(this.pressTimer),this.released?this.close():this.release())},clickOverlay:function(){this.close()},resizeWindow:function(){this.close()}};function d(t){return 0===t.button}function f(t){return t.metaKey||t.ctrlKey}var p={init:function(t){this.el=document.createElement("div"),this.instance=t,this.parent=document.body,c(this.el,{position:"fixed",top:0,left:0,right:0,bottom:0,opacity:0}),this.updateStyle(t.options),l(this.el,"click",t.handler.clickOverlay.bind(t))},updateStyle:function(t){c(this.el,{zIndex:t.zIndex,backgroundColor:t.bgColor,transition:"opacity\n "+t.transitionDuration+"s\n "+t.transitionTimingFunction})},insert:function(){this.parent.appendChild(this.el)},remove:function(){this.parent.removeChild(this.el)},fadeIn:function(){this.el.offsetWidth,this.el.style.opacity=this.instance.options.bgOpacity},fadeOut:function(){this.el.style.opacity=0}},m="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},y=function(){function n(t,e){for(var i=0;i 3 && arguments[3] !== undefined ? arguments[3] : true; 11 | 12 | var options = { passive: false }; 13 | 14 | if (add) { 15 | el.addEventListener(event, handler, options); 16 | } else { 17 | el.removeEventListener(event, handler, options); 18 | } 19 | } 20 | 21 | function loadImage(src, cb) { 22 | if (src) { 23 | var img = new Image(); 24 | 25 | img.onload = function onImageLoad() { 26 | if (cb) cb(img); 27 | }; 28 | 29 | img.src = src; 30 | } 31 | } 32 | 33 | function getOriginalSource(el) { 34 | if (el.dataset.original) { 35 | return el.dataset.original; 36 | } else if (el.parentNode.tagName === 'A') { 37 | return el.parentNode.getAttribute('href'); 38 | } else { 39 | return null; 40 | } 41 | } 42 | 43 | function setStyle(el, styles, remember) { 44 | if (styles.transition) { 45 | var value = styles.transition; 46 | delete styles.transition; 47 | styles.transition = value; 48 | } 49 | 50 | if (styles.transform) { 51 | var _value = styles.transform; 52 | delete styles.transform; 53 | styles.transform = _value; 54 | } 55 | 56 | var s = el.style; 57 | var original = {}; 58 | 59 | for (var key in styles) { 60 | if (remember) { 61 | original[key] = s[key] || ''; 62 | } 63 | 64 | s[key] = styles[key]; 65 | } 66 | 67 | return original; 68 | } 69 | 70 | function bindAll(_this, that) { 71 | var methods = Object.getOwnPropertyNames(Object.getPrototypeOf(_this)); 72 | methods.forEach(function bindOne(method) { 73 | _this[method] = _this[method].bind(that); 74 | }); 75 | } 76 | 77 | var noop = function noop() {}; 78 | 79 | var DEFAULT_OPTIONS = { 80 | /** 81 | * To be able to grab and drag the image for extra zoom-in. 82 | * @type {boolean} 83 | */ 84 | enableGrab: true, 85 | 86 | /** 87 | * Preload zoomable images. 88 | * @type {boolean} 89 | */ 90 | preloadImage: false, 91 | 92 | /** 93 | * Close the zoomed image when browser window is resized. 94 | * @type {boolean} 95 | */ 96 | closeOnWindowResize: true, 97 | 98 | /** 99 | * Transition duration in seconds. 100 | * @type {number} 101 | */ 102 | transitionDuration: 0.4, 103 | 104 | /** 105 | * Transition timing function. 106 | * @type {string} 107 | */ 108 | transitionTimingFunction: 'cubic-bezier(0.4, 0, 0, 1)', 109 | 110 | /** 111 | * Overlay background color. 112 | * @type {string} 113 | */ 114 | bgColor: 'rgb(255, 255, 255)', 115 | 116 | /** 117 | * Overlay background opacity. 118 | * @type {number} 119 | */ 120 | bgOpacity: 1, 121 | 122 | /** 123 | * The base scale factor for zooming. By default scale to fit the window. 124 | * @type {number} 125 | */ 126 | scaleBase: 1.0, 127 | 128 | /** 129 | * The additional scale factor when grabbing the image. 130 | * @type {number} 131 | */ 132 | scaleExtra: 0.5, 133 | 134 | /** 135 | * How much scrolling it takes before closing out. 136 | * @type {number} 137 | */ 138 | scrollThreshold: 40, 139 | 140 | /** 141 | * The z-index that the overlay will be added with. 142 | * @type {number} 143 | */ 144 | zIndex: 998, 145 | 146 | /** 147 | * Scale (zoom in) to given width and height. Ignore scaleBase if set. 148 | * Alternatively, provide a percentage value relative to the original image size. 149 | * @type {Object|String} 150 | * @example 151 | * customSize: { width: 800, height: 400 } 152 | * customSize: 100% 153 | */ 154 | customSize: null, 155 | 156 | /** 157 | * A callback function that will be called when a target is opened and 158 | * transition has ended. It will get the target element as the argument. 159 | * @type {Function} 160 | */ 161 | onOpen: noop, 162 | 163 | /** 164 | * Same as above, except fired when closed. 165 | * @type {Function} 166 | */ 167 | onClose: noop, 168 | 169 | /** 170 | * Same as above, except fired when grabbed. 171 | * @type {Function} 172 | */ 173 | onGrab: noop, 174 | 175 | /** 176 | * Same as above, except fired when moved. 177 | * @type {Function} 178 | */ 179 | onMove: noop, 180 | 181 | /** 182 | * Same as above, except fired when released. 183 | * @type {Function} 184 | */ 185 | onRelease: noop, 186 | 187 | /** 188 | * A callback function that will be called before open. 189 | * @type {Function} 190 | */ 191 | onBeforeOpen: noop, 192 | 193 | /** 194 | * A callback function that will be called before close. 195 | * @type {Function} 196 | */ 197 | onBeforeClose: noop, 198 | 199 | /** 200 | * A callback function that will be called before grab. 201 | * @type {Function} 202 | */ 203 | onBeforeGrab: noop, 204 | 205 | /** 206 | * A callback function that will be called before release. 207 | * @type {Function} 208 | */ 209 | onBeforeRelease: noop, 210 | 211 | /** 212 | * A callback function that will be called when the hi-res image is loading. 213 | * @type {Function} 214 | */ 215 | onImageLoading: noop, 216 | 217 | /** 218 | * A callback function that will be called when the hi-res image is loaded. 219 | * @type {Function} 220 | */ 221 | onImageLoaded: noop 222 | }; 223 | 224 | var PRESS_DELAY = 200; 225 | 226 | var handler = { 227 | init: function init(instance) { 228 | bindAll(this, instance); 229 | }, 230 | click: function click(e) { 231 | e.preventDefault(); 232 | 233 | if (isPressingMetaKey(e)) { 234 | return window.open(this.target.srcOriginal || e.currentTarget.src, '_blank'); 235 | } else { 236 | if (this.shown) { 237 | if (this.released) { 238 | this.close(); 239 | } else { 240 | this.release(); 241 | } 242 | } else { 243 | this.open(e.currentTarget); 244 | } 245 | } 246 | }, 247 | scroll: function scroll() { 248 | var el = document.documentElement || document.body.parentNode || document.body; 249 | var scrollLeft = window.pageXOffset || el.scrollLeft; 250 | var scrollTop = window.pageYOffset || el.scrollTop; 251 | 252 | if (this.lastScrollPosition === null) { 253 | this.lastScrollPosition = { 254 | x: scrollLeft, 255 | y: scrollTop 256 | }; 257 | } 258 | 259 | var deltaX = this.lastScrollPosition.x - scrollLeft; 260 | var deltaY = this.lastScrollPosition.y - scrollTop; 261 | var threshold = this.options.scrollThreshold; 262 | 263 | if (Math.abs(deltaY) >= threshold || Math.abs(deltaX) >= threshold) { 264 | this.lastScrollPosition = null; 265 | this.close(); 266 | } 267 | }, 268 | keydown: function keydown(e) { 269 | if (isEscape(e)) { 270 | if (this.released) { 271 | this.close(); 272 | } else { 273 | this.release(this.close); 274 | } 275 | } 276 | }, 277 | mousedown: function mousedown(e) { 278 | if (!isLeftButton(e) || isPressingMetaKey(e)) return; 279 | e.preventDefault(); 280 | var clientX = e.clientX, 281 | clientY = e.clientY; 282 | 283 | 284 | this.pressTimer = setTimeout(function grabOnMouseDown() { 285 | this.grab(clientX, clientY); 286 | }.bind(this), PRESS_DELAY); 287 | }, 288 | mousemove: function mousemove(e) { 289 | if (this.released) return; 290 | this.move(e.clientX, e.clientY); 291 | }, 292 | mouseup: function mouseup(e) { 293 | if (!isLeftButton(e) || isPressingMetaKey(e)) return; 294 | clearTimeout(this.pressTimer); 295 | 296 | if (this.released) { 297 | this.close(); 298 | } else { 299 | this.release(); 300 | } 301 | }, 302 | touchstart: function touchstart(e) { 303 | e.preventDefault(); 304 | var _e$touches$ = e.touches[0], 305 | clientX = _e$touches$.clientX, 306 | clientY = _e$touches$.clientY; 307 | 308 | 309 | this.pressTimer = setTimeout(function grabOnTouchStart() { 310 | this.grab(clientX, clientY); 311 | }.bind(this), PRESS_DELAY); 312 | }, 313 | touchmove: function touchmove(e) { 314 | if (this.released) return; 315 | 316 | var _e$touches$2 = e.touches[0], 317 | clientX = _e$touches$2.clientX, 318 | clientY = _e$touches$2.clientY; 319 | 320 | this.move(clientX, clientY); 321 | }, 322 | touchend: function touchend(e) { 323 | if (isTouching(e)) return; 324 | clearTimeout(this.pressTimer); 325 | 326 | if (this.released) { 327 | this.close(); 328 | } else { 329 | this.release(); 330 | } 331 | }, 332 | clickOverlay: function clickOverlay() { 333 | this.close(); 334 | }, 335 | resizeWindow: function resizeWindow() { 336 | this.close(); 337 | } 338 | }; 339 | 340 | function isLeftButton(e) { 341 | return e.button === 0; 342 | } 343 | 344 | function isPressingMetaKey(e) { 345 | return e.metaKey || e.ctrlKey; 346 | } 347 | 348 | function isTouching(e) { 349 | e.targetTouches.length > 0; 350 | } 351 | 352 | function isEscape(e) { 353 | var code = e.key || e.code; 354 | return code === 'Escape' || e.keyCode === 27; 355 | } 356 | 357 | var overlay = { 358 | init: function init(instance) { 359 | this.el = document.createElement('div'); 360 | this.instance = instance; 361 | this.parent = document.body; 362 | 363 | setStyle(this.el, { 364 | position: 'fixed', 365 | top: 0, 366 | left: 0, 367 | right: 0, 368 | bottom: 0, 369 | opacity: 0 370 | }); 371 | 372 | this.updateStyle(instance.options); 373 | listen(this.el, 'click', instance.handler.clickOverlay.bind(instance)); 374 | }, 375 | updateStyle: function updateStyle(options) { 376 | setStyle(this.el, { 377 | zIndex: options.zIndex, 378 | backgroundColor: options.bgColor, 379 | transition: 'opacity\n ' + options.transitionDuration + 's\n ' + options.transitionTimingFunction 380 | }); 381 | }, 382 | insert: function insert() { 383 | this.parent.appendChild(this.el); 384 | }, 385 | remove: function remove() { 386 | this.parent.removeChild(this.el); 387 | }, 388 | fadeIn: function fadeIn() { 389 | this.el.offsetWidth; 390 | this.el.style.opacity = this.instance.options.bgOpacity; 391 | }, 392 | fadeOut: function fadeOut() { 393 | this.el.style.opacity = 0; 394 | } 395 | }; 396 | 397 | var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { 398 | return typeof obj; 399 | } : function (obj) { 400 | return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; 401 | }; 402 | 403 | var classCallCheck = function (instance, Constructor) { 404 | if (!(instance instanceof Constructor)) { 405 | throw new TypeError("Cannot call a class as a function"); 406 | } 407 | }; 408 | 409 | var createClass = function () { 410 | function defineProperties(target, props) { 411 | for (var i = 0; i < props.length; i++) { 412 | var descriptor = props[i]; 413 | descriptor.enumerable = descriptor.enumerable || false; 414 | descriptor.configurable = true; 415 | if ("value" in descriptor) descriptor.writable = true; 416 | Object.defineProperty(target, descriptor.key, descriptor); 417 | } 418 | } 419 | 420 | return function (Constructor, protoProps, staticProps) { 421 | if (protoProps) defineProperties(Constructor.prototype, protoProps); 422 | if (staticProps) defineProperties(Constructor, staticProps); 423 | return Constructor; 424 | }; 425 | }(); 426 | 427 | var _extends = Object.assign || function (target) { 428 | for (var i = 1; i < arguments.length; i++) { 429 | var source = arguments[i]; 430 | 431 | for (var key in source) { 432 | if (Object.prototype.hasOwnProperty.call(source, key)) { 433 | target[key] = source[key]; 434 | } 435 | } 436 | } 437 | 438 | return target; 439 | }; 440 | 441 | // Translate z-axis to fix CSS grid display issue in Chrome: 442 | // https://github.com/kingdido999/zooming/issues/42 443 | var TRANSLATE_Z = 0; 444 | 445 | var target = { 446 | init: function init(el, instance) { 447 | this.el = el; 448 | this.instance = instance; 449 | this.srcThumbnail = this.el.getAttribute('src'); 450 | this.srcset = this.el.getAttribute('srcset'); 451 | this.srcOriginal = getOriginalSource(this.el); 452 | this.rect = this.el.getBoundingClientRect(); 453 | this.translate = null; 454 | this.scale = null; 455 | this.styleOpen = null; 456 | this.styleClose = null; 457 | }, 458 | zoomIn: function zoomIn() { 459 | var _instance$options = this.instance.options, 460 | zIndex = _instance$options.zIndex, 461 | enableGrab = _instance$options.enableGrab, 462 | transitionDuration = _instance$options.transitionDuration, 463 | transitionTimingFunction = _instance$options.transitionTimingFunction; 464 | 465 | this.translate = this.calculateTranslate(); 466 | this.scale = this.calculateScale(); 467 | 468 | this.styleOpen = { 469 | position: 'relative', 470 | zIndex: zIndex + 1, 471 | cursor: enableGrab ? cursor.grab : cursor.zoomOut, 472 | transition: 'transform\n ' + transitionDuration + 's\n ' + transitionTimingFunction, 473 | transform: 'translate3d(' + this.translate.x + 'px, ' + this.translate.y + 'px, ' + TRANSLATE_Z + 'px)\n scale(' + this.scale.x + ',' + this.scale.y + ')', 474 | height: this.rect.height + 'px', 475 | width: this.rect.width + 'px' 476 | 477 | // Force layout update 478 | };this.el.offsetWidth; 479 | 480 | // Trigger transition 481 | this.styleClose = setStyle(this.el, this.styleOpen, true); 482 | }, 483 | zoomOut: function zoomOut() { 484 | // Force layout update 485 | this.el.offsetWidth; 486 | 487 | setStyle(this.el, { transform: 'none' }); 488 | }, 489 | grab: function grab(x, y, scaleExtra) { 490 | var windowCenter = getWindowCenter(); 491 | var dx = windowCenter.x - x, 492 | dy = windowCenter.y - y; 493 | 494 | 495 | setStyle(this.el, { 496 | cursor: cursor.move, 497 | transform: 'translate3d(\n ' + (this.translate.x + dx) + 'px, ' + (this.translate.y + dy) + 'px, ' + TRANSLATE_Z + 'px)\n scale(' + (this.scale.x + scaleExtra) + ',' + (this.scale.y + scaleExtra) + ')' 498 | }); 499 | }, 500 | move: function move(x, y, scaleExtra) { 501 | var windowCenter = getWindowCenter(); 502 | var dx = windowCenter.x - x, 503 | dy = windowCenter.y - y; 504 | 505 | 506 | setStyle(this.el, { 507 | transition: 'transform', 508 | transform: 'translate3d(\n ' + (this.translate.x + dx) + 'px, ' + (this.translate.y + dy) + 'px, ' + TRANSLATE_Z + 'px)\n scale(' + (this.scale.x + scaleExtra) + ',' + (this.scale.y + scaleExtra) + ')' 509 | }); 510 | }, 511 | restoreCloseStyle: function restoreCloseStyle() { 512 | setStyle(this.el, this.styleClose); 513 | }, 514 | restoreOpenStyle: function restoreOpenStyle() { 515 | setStyle(this.el, this.styleOpen); 516 | }, 517 | upgradeSource: function upgradeSource() { 518 | if (this.srcOriginal) { 519 | var parentNode = this.el.parentNode; 520 | 521 | if (this.srcset) { 522 | this.el.removeAttribute('srcset'); 523 | } 524 | 525 | var temp = this.el.cloneNode(false); 526 | 527 | // Force compute the hi-res image in DOM to prevent 528 | // image flickering while updating src 529 | temp.setAttribute('src', this.srcOriginal); 530 | temp.style.position = 'fixed'; 531 | temp.style.visibility = 'hidden'; 532 | parentNode.appendChild(temp); 533 | 534 | // Add delay to prevent Firefox from flickering 535 | setTimeout(function updateSrc() { 536 | this.el.setAttribute('src', this.srcOriginal); 537 | parentNode.removeChild(temp); 538 | }.bind(this), 50); 539 | } 540 | }, 541 | downgradeSource: function downgradeSource() { 542 | if (this.srcOriginal) { 543 | if (this.srcset) { 544 | this.el.setAttribute('srcset', this.srcset); 545 | } 546 | this.el.setAttribute('src', this.srcThumbnail); 547 | } 548 | }, 549 | calculateTranslate: function calculateTranslate() { 550 | var windowCenter = getWindowCenter(); 551 | var targetCenter = { 552 | x: this.rect.left + this.rect.width / 2, 553 | y: this.rect.top + this.rect.height / 2 554 | 555 | // The vector to translate image to the window center 556 | };return { 557 | x: windowCenter.x - targetCenter.x, 558 | y: windowCenter.y - targetCenter.y 559 | }; 560 | }, 561 | calculateScale: function calculateScale() { 562 | var _el$dataset = this.el.dataset, 563 | zoomingHeight = _el$dataset.zoomingHeight, 564 | zoomingWidth = _el$dataset.zoomingWidth; 565 | var _instance$options2 = this.instance.options, 566 | customSize = _instance$options2.customSize, 567 | scaleBase = _instance$options2.scaleBase; 568 | 569 | 570 | if (!customSize && zoomingHeight && zoomingWidth) { 571 | return { 572 | x: zoomingWidth / this.rect.width, 573 | y: zoomingHeight / this.rect.height 574 | }; 575 | } else if (customSize && (typeof customSize === 'undefined' ? 'undefined' : _typeof(customSize)) === 'object') { 576 | return { 577 | x: customSize.width / this.rect.width, 578 | y: customSize.height / this.rect.height 579 | }; 580 | } else { 581 | var targetHalfWidth = this.rect.width / 2; 582 | var targetHalfHeight = this.rect.height / 2; 583 | var windowCenter = getWindowCenter(); 584 | 585 | // The distance between target edge and window edge 586 | var targetEdgeToWindowEdge = { 587 | x: windowCenter.x - targetHalfWidth, 588 | y: windowCenter.y - targetHalfHeight 589 | }; 590 | 591 | var scaleHorizontally = targetEdgeToWindowEdge.x / targetHalfWidth; 592 | var scaleVertically = targetEdgeToWindowEdge.y / targetHalfHeight; 593 | 594 | // The additional scale is based on the smaller value of 595 | // scaling horizontally and scaling vertically 596 | var scale = scaleBase + Math.min(scaleHorizontally, scaleVertically); 597 | 598 | if (customSize && typeof customSize === 'string') { 599 | // Use zoomingWidth and zoomingHeight if available 600 | var naturalWidth = zoomingWidth || this.el.naturalWidth; 601 | var naturalHeight = zoomingHeight || this.el.naturalHeight; 602 | var maxZoomingWidth = parseFloat(customSize) * naturalWidth / (100 * this.rect.width); 603 | var maxZoomingHeight = parseFloat(customSize) * naturalHeight / (100 * this.rect.height); 604 | 605 | // Only scale image up to the specified customSize percentage 606 | if (scale > maxZoomingWidth || scale > maxZoomingHeight) { 607 | return { 608 | x: maxZoomingWidth, 609 | y: maxZoomingHeight 610 | }; 611 | } 612 | } 613 | 614 | return { 615 | x: scale, 616 | y: scale 617 | }; 618 | } 619 | } 620 | }; 621 | 622 | function getWindowCenter() { 623 | var docEl = document.documentElement; 624 | var windowWidth = Math.min(docEl.clientWidth, window.innerWidth); 625 | var windowHeight = Math.min(docEl.clientHeight, window.innerHeight); 626 | 627 | return { 628 | x: windowWidth / 2, 629 | y: windowHeight / 2 630 | }; 631 | } 632 | 633 | /** 634 | * Zooming instance. 635 | */ 636 | 637 | var Zooming = function () { 638 | /** 639 | * @param {Object} [options] Update default options if provided. 640 | */ 641 | function Zooming(options) { 642 | classCallCheck(this, Zooming); 643 | 644 | this.target = Object.create(target); 645 | this.overlay = Object.create(overlay); 646 | this.handler = Object.create(handler); 647 | this.body = document.body; 648 | 649 | this.shown = false; 650 | this.lock = false; 651 | this.released = true; 652 | this.lastScrollPosition = null; 653 | this.pressTimer = null; 654 | 655 | this.options = _extends({}, DEFAULT_OPTIONS, options); 656 | this.overlay.init(this); 657 | this.handler.init(this); 658 | } 659 | 660 | /** 661 | * Make element(s) zoomable. 662 | * @param {string|Element} el A css selector or an Element. 663 | * @return {this} 664 | */ 665 | 666 | 667 | createClass(Zooming, [{ 668 | key: 'listen', 669 | value: function listen$$1(el) { 670 | if (typeof el === 'string') { 671 | var els = document.querySelectorAll(el); 672 | var i = els.length; 673 | 674 | while (i--) { 675 | this.listen(els[i]); 676 | } 677 | } else if (el.tagName === 'IMG') { 678 | el.style.cursor = cursor.zoomIn; 679 | listen(el, 'click', this.handler.click); 680 | 681 | if (this.options.preloadImage) { 682 | loadImage(getOriginalSource(el)); 683 | } 684 | } 685 | 686 | return this; 687 | } 688 | 689 | /** 690 | * Update options or return current options if no argument is provided. 691 | * @param {Object} options An Object that contains this.options. 692 | * @return {this|this.options} 693 | */ 694 | 695 | }, { 696 | key: 'config', 697 | value: function config(options) { 698 | if (options) { 699 | _extends(this.options, options); 700 | this.overlay.updateStyle(this.options); 701 | return this; 702 | } else { 703 | return this.options; 704 | } 705 | } 706 | 707 | /** 708 | * Open (zoom in) the Element. 709 | * @param {Element} el The Element to open. 710 | * @param {Function} [cb=this.options.onOpen] A callback function that will 711 | * be called when a target is opened and transition has ended. It will get 712 | * the target element as the argument. 713 | * @return {this} 714 | */ 715 | 716 | }, { 717 | key: 'open', 718 | value: function open(el) { 719 | var _this = this; 720 | 721 | var cb = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : this.options.onOpen; 722 | 723 | if (this.shown || this.lock) return; 724 | 725 | var target$$1 = typeof el === 'string' ? document.querySelector(el) : el; 726 | 727 | if (target$$1.tagName !== 'IMG') return; 728 | 729 | this.options.onBeforeOpen(target$$1); 730 | 731 | this.target.init(target$$1, this); 732 | 733 | if (!this.options.preloadImage) { 734 | var srcOriginal = this.target.srcOriginal; 735 | 736 | 737 | if (srcOriginal != null) { 738 | this.options.onImageLoading(target$$1); 739 | loadImage(srcOriginal, this.options.onImageLoaded); 740 | } 741 | } 742 | 743 | this.shown = true; 744 | this.lock = true; 745 | 746 | this.target.zoomIn(); 747 | this.overlay.insert(); 748 | this.overlay.fadeIn(); 749 | 750 | listen(document, 'scroll', this.handler.scroll); 751 | listen(document, 'keydown', this.handler.keydown); 752 | 753 | if (this.options.closeOnWindowResize) { 754 | listen(window, 'resize', this.handler.resizeWindow); 755 | } 756 | 757 | var onOpenEnd = function onOpenEnd() { 758 | listen(target$$1, 'transitionend', onOpenEnd, false); 759 | _this.lock = false; 760 | _this.target.upgradeSource(); 761 | 762 | if (_this.options.enableGrab) { 763 | toggleGrabListeners(document, _this.handler, true); 764 | } 765 | 766 | cb(target$$1); 767 | }; 768 | 769 | listen(target$$1, 'transitionend', onOpenEnd); 770 | 771 | return this; 772 | } 773 | 774 | /** 775 | * Close (zoom out) the Element currently opened. 776 | * @param {Function} [cb=this.options.onClose] A callback function that will 777 | * be called when a target is closed and transition has ended. It will get 778 | * the target element as the argument. 779 | * @return {this} 780 | */ 781 | 782 | }, { 783 | key: 'close', 784 | value: function close() { 785 | var _this2 = this; 786 | 787 | var cb = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.options.onClose; 788 | 789 | if (!this.shown || this.lock) return; 790 | 791 | var target$$1 = this.target.el; 792 | 793 | this.options.onBeforeClose(target$$1); 794 | 795 | this.lock = true; 796 | this.body.style.cursor = cursor.default; 797 | this.overlay.fadeOut(); 798 | this.target.zoomOut(); 799 | 800 | listen(document, 'scroll', this.handler.scroll, false); 801 | listen(document, 'keydown', this.handler.keydown, false); 802 | 803 | if (this.options.closeOnWindowResize) { 804 | listen(window, 'resize', this.handler.resizeWindow, false); 805 | } 806 | 807 | var onCloseEnd = function onCloseEnd() { 808 | listen(target$$1, 'transitionend', onCloseEnd, false); 809 | 810 | _this2.shown = false; 811 | _this2.lock = false; 812 | 813 | _this2.target.downgradeSource(); 814 | 815 | if (_this2.options.enableGrab) { 816 | toggleGrabListeners(document, _this2.handler, false); 817 | } 818 | 819 | _this2.target.restoreCloseStyle(); 820 | _this2.overlay.remove(); 821 | 822 | cb(target$$1); 823 | }; 824 | 825 | listen(target$$1, 'transitionend', onCloseEnd); 826 | 827 | return this; 828 | } 829 | 830 | /** 831 | * Grab the Element currently opened given a position and apply extra zoom-in. 832 | * @param {number} x The X-axis of where the press happened. 833 | * @param {number} y The Y-axis of where the press happened. 834 | * @param {number} scaleExtra Extra zoom-in to apply. 835 | * @param {Function} [cb=this.options.onGrab] A callback function that 836 | * will be called when a target is grabbed and transition has ended. It 837 | * will get the target element as the argument. 838 | * @return {this} 839 | */ 840 | 841 | }, { 842 | key: 'grab', 843 | value: function grab(x, y) { 844 | var scaleExtra = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : this.options.scaleExtra; 845 | var cb = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : this.options.onGrab; 846 | 847 | if (!this.shown || this.lock) return; 848 | 849 | var target$$1 = this.target.el; 850 | 851 | this.options.onBeforeGrab(target$$1); 852 | 853 | this.released = false; 854 | this.target.grab(x, y, scaleExtra); 855 | 856 | var onGrabEnd = function onGrabEnd() { 857 | listen(target$$1, 'transitionend', onGrabEnd, false); 858 | cb(target$$1); 859 | }; 860 | 861 | listen(target$$1, 'transitionend', onGrabEnd); 862 | 863 | return this; 864 | } 865 | 866 | /** 867 | * Move the Element currently grabbed given a position and apply extra zoom-in. 868 | * @param {number} x The X-axis of where the press happened. 869 | * @param {number} y The Y-axis of where the press happened. 870 | * @param {number} scaleExtra Extra zoom-in to apply. 871 | * @param {Function} [cb=this.options.onMove] A callback function that 872 | * will be called when a target is moved and transition has ended. It will 873 | * get the target element as the argument. 874 | * @return {this} 875 | */ 876 | 877 | }, { 878 | key: 'move', 879 | value: function move(x, y) { 880 | var scaleExtra = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : this.options.scaleExtra; 881 | var cb = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : this.options.onMove; 882 | 883 | if (!this.shown || this.lock) return; 884 | 885 | this.released = false; 886 | this.body.style.cursor = cursor.move; 887 | this.target.move(x, y, scaleExtra); 888 | 889 | var target$$1 = this.target.el; 890 | 891 | var onMoveEnd = function onMoveEnd() { 892 | listen(target$$1, 'transitionend', onMoveEnd, false); 893 | cb(target$$1); 894 | }; 895 | 896 | listen(target$$1, 'transitionend', onMoveEnd); 897 | 898 | return this; 899 | } 900 | 901 | /** 902 | * Release the Element currently grabbed. 903 | * @param {Function} [cb=this.options.onRelease] A callback function that 904 | * will be called when a target is released and transition has ended. It 905 | * will get the target element as the argument. 906 | * @return {this} 907 | */ 908 | 909 | }, { 910 | key: 'release', 911 | value: function release() { 912 | var _this3 = this; 913 | 914 | var cb = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.options.onRelease; 915 | 916 | if (!this.shown || this.lock) return; 917 | 918 | var target$$1 = this.target.el; 919 | 920 | this.options.onBeforeRelease(target$$1); 921 | 922 | this.lock = true; 923 | this.body.style.cursor = cursor.default; 924 | this.target.restoreOpenStyle(); 925 | 926 | var onReleaseEnd = function onReleaseEnd() { 927 | listen(target$$1, 'transitionend', onReleaseEnd, false); 928 | _this3.lock = false; 929 | _this3.released = true; 930 | cb(target$$1); 931 | }; 932 | 933 | listen(target$$1, 'transitionend', onReleaseEnd); 934 | 935 | return this; 936 | } 937 | }]); 938 | return Zooming; 939 | }(); 940 | 941 | 942 | function toggleGrabListeners(el, handler$$1, add) { 943 | var types = ['mousedown', 'mousemove', 'mouseup', 'touchstart', 'touchmove', 'touchend']; 944 | 945 | types.forEach(function toggleListener(type) { 946 | listen(el, type, handler$$1[type], add); 947 | }); 948 | } 949 | 950 | export default Zooming; 951 | //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiem9vbWluZy5tb2R1bGUuanMiLCJzb3VyY2VzIjpbIi4uL3NyYy91dGlscy5qcyIsIi4uL3NyYy9vcHRpb25zLmpzIiwiLi4vc3JjL2hhbmRsZXIuanMiLCIuLi9zcmMvb3ZlcmxheS5qcyIsIi4uL3NyYy90YXJnZXQuanMiLCIuLi9zcmMvaW5kZXguanMiXSwic291cmNlc0NvbnRlbnQiOlsiZXhwb3J0IGNvbnN0IGN1cnNvciA9IHtcbiAgZGVmYXVsdDogJ2F1dG8nLFxuICB6b29tSW46ICd6b29tLWluJyxcbiAgem9vbU91dDogJ3pvb20tb3V0JyxcbiAgZ3JhYjogJ2dyYWInLFxuICBtb3ZlOiAnbW92ZSdcbn1cblxuZXhwb3J0IGZ1bmN0aW9uIGxpc3RlbihlbCwgZXZlbnQsIGhhbmRsZXIsIGFkZCA9IHRydWUpIHtcbiAgY29uc3Qgb3B0aW9ucyA9IHsgcGFzc2l2ZTogZmFsc2UgfVxuXG4gIGlmIChhZGQpIHtcbiAgICBlbC5hZGRFdmVudExpc3RlbmVyKGV2ZW50LCBoYW5kbGVyLCBvcHRpb25zKVxuICB9IGVsc2Uge1xuICAgIGVsLnJlbW92ZUV2ZW50TGlzdGVuZXIoZXZlbnQsIGhhbmRsZXIsIG9wdGlvbnMpXG4gIH1cbn1cblxuZXhwb3J0IGZ1bmN0aW9uIGxvYWRJbWFnZShzcmMsIGNiKSB7XG4gIGlmIChzcmMpIHtcbiAgICBjb25zdCBpbWcgPSBuZXcgSW1hZ2UoKVxuXG4gICAgaW1nLm9ubG9hZCA9IGZ1bmN0aW9uIG9uSW1hZ2VMb2FkKCkge1xuICAgICAgaWYgKGNiKSBjYihpbWcpXG4gICAgfVxuXG4gICAgaW1nLnNyYyA9IHNyY1xuICB9XG59XG5cbmV4cG9ydCBmdW5jdGlvbiBnZXRPcmlnaW5hbFNvdXJjZShlbCkge1xuICBpZiAoZWwuZGF0YXNldC5vcmlnaW5hbCkge1xuICAgIHJldHVybiBlbC5kYXRhc2V0Lm9yaWdpbmFsXG4gIH0gZWxzZSBpZiAoZWwucGFyZW50Tm9kZS50YWdOYW1lID09PSAnQScpIHtcbiAgICByZXR1cm4gZWwucGFyZW50Tm9kZS5nZXRBdHRyaWJ1dGUoJ2hyZWYnKVxuICB9IGVsc2Uge1xuICAgIHJldHVybiBudWxsXG4gIH1cbn1cblxuZXhwb3J0IGZ1bmN0aW9uIHNldFN0eWxlKGVsLCBzdHlsZXMsIHJlbWVtYmVyKSB7XG4gIGlmIChzdHlsZXMudHJhbnNpdGlvbikge1xuICAgIGNvbnN0IHZhbHVlID0gc3R5bGVzLnRyYW5zaXRpb25cbiAgICBkZWxldGUgc3R5bGVzLnRyYW5zaXRpb25cbiAgICBzdHlsZXMudHJhbnNpdGlvbiA9IHZhbHVlXG4gIH1cblxuICBpZiAoc3R5bGVzLnRyYW5zZm9ybSkge1xuICAgIGNvbnN0IHZhbHVlID0gc3R5bGVzLnRyYW5zZm9ybVxuICAgIGRlbGV0ZSBzdHlsZXMudHJhbnNmb3JtXG4gICAgc3R5bGVzLnRyYW5zZm9ybSA9IHZhbHVlXG4gIH1cblxuICBsZXQgcyA9IGVsLnN0eWxlXG4gIGxldCBvcmlnaW5hbCA9IHt9XG5cbiAgZm9yIChsZXQga2V5IGluIHN0eWxlcykge1xuICAgIGlmIChyZW1lbWJlcikge1xuICAgICAgb3JpZ2luYWxba2V5XSA9IHNba2V5XSB8fCAnJ1xuICAgIH1cblxuICAgIHNba2V5XSA9IHN0eWxlc1trZXldXG4gIH1cblxuICByZXR1cm4gb3JpZ2luYWxcbn1cblxuZXhwb3J0IGZ1bmN0aW9uIGJpbmRBbGwoX3RoaXMsIHRoYXQpIHtcbiAgY29uc3QgbWV0aG9kcyA9IE9iamVjdC5nZXRPd25Qcm9wZXJ0eU5hbWVzKE9iamVjdC5nZXRQcm90b3R5cGVPZihfdGhpcykpXG4gIG1ldGhvZHMuZm9yRWFjaChmdW5jdGlvbiBiaW5kT25lKG1ldGhvZCkge1xuICAgIF90aGlzW21ldGhvZF0gPSBfdGhpc1ttZXRob2RdLmJpbmQodGhhdClcbiAgfSlcbn1cbiIsImNvbnN0IG5vb3AgPSAoKSA9PiB7fVxuXG5leHBvcnQgZGVmYXVsdCB7XG4gIC8qKlxuICAgKiBUbyBiZSBhYmxlIHRvIGdyYWIgYW5kIGRyYWcgdGhlIGltYWdlIGZvciBleHRyYSB6b29tLWluLlxuICAgKiBAdHlwZSB7Ym9vbGVhbn1cbiAgICovXG4gIGVuYWJsZUdyYWI6IHRydWUsXG5cbiAgLyoqXG4gICAqIFByZWxvYWQgem9vbWFibGUgaW1hZ2VzLlxuICAgKiBAdHlwZSB7Ym9vbGVhbn1cbiAgICovXG4gIHByZWxvYWRJbWFnZTogZmFsc2UsXG5cbiAgLyoqXG4gICAqIENsb3NlIHRoZSB6b29tZWQgaW1hZ2Ugd2hlbiBicm93c2VyIHdpbmRvdyBpcyByZXNpemVkLlxuICAgKiBAdHlwZSB7Ym9vbGVhbn1cbiAgICovXG4gIGNsb3NlT25XaW5kb3dSZXNpemU6IHRydWUsXG5cbiAgLyoqXG4gICAqIFRyYW5zaXRpb24gZHVyYXRpb24gaW4gc2Vjb25kcy5cbiAgICogQHR5cGUge251bWJlcn1cbiAgICovXG4gIHRyYW5zaXRpb25EdXJhdGlvbjogMC40LFxuXG4gIC8qKlxuICAgKiBUcmFuc2l0aW9uIHRpbWluZyBmdW5jdGlvbi5cbiAgICogQHR5cGUge3N0cmluZ31cbiAgICovXG4gIHRyYW5zaXRpb25UaW1pbmdGdW5jdGlvbjogJ2N1YmljLWJlemllcigwLjQsIDAsIDAsIDEpJyxcblxuICAvKipcbiAgICogT3ZlcmxheSBiYWNrZ3JvdW5kIGNvbG9yLlxuICAgKiBAdHlwZSB7c3RyaW5nfVxuICAgKi9cbiAgYmdDb2xvcjogJ3JnYigyNTUsIDI1NSwgMjU1KScsXG5cbiAgLyoqXG4gICAqIE92ZXJsYXkgYmFja2dyb3VuZCBvcGFjaXR5LlxuICAgKiBAdHlwZSB7bnVtYmVyfVxuICAgKi9cbiAgYmdPcGFjaXR5OiAxLFxuXG4gIC8qKlxuICAgKiBUaGUgYmFzZSBzY2FsZSBmYWN0b3IgZm9yIHpvb21pbmcuIEJ5IGRlZmF1bHQgc2NhbGUgdG8gZml0IHRoZSB3aW5kb3cuXG4gICAqIEB0eXBlIHtudW1iZXJ9XG4gICAqL1xuICBzY2FsZUJhc2U6IDEuMCxcblxuICAvKipcbiAgICogVGhlIGFkZGl0aW9uYWwgc2NhbGUgZmFjdG9yIHdoZW4gZ3JhYmJpbmcgdGhlIGltYWdlLlxuICAgKiBAdHlwZSB7bnVtYmVyfVxuICAgKi9cbiAgc2NhbGVFeHRyYTogMC41LFxuXG4gIC8qKlxuICAgKiBIb3cgbXVjaCBzY3JvbGxpbmcgaXQgdGFrZXMgYmVmb3JlIGNsb3Npbmcgb3V0LlxuICAgKiBAdHlwZSB7bnVtYmVyfVxuICAgKi9cbiAgc2Nyb2xsVGhyZXNob2xkOiA0MCxcblxuICAvKipcbiAgICogVGhlIHotaW5kZXggdGhhdCB0aGUgb3ZlcmxheSB3aWxsIGJlIGFkZGVkIHdpdGguXG4gICAqIEB0eXBlIHtudW1iZXJ9XG4gICAqL1xuICB6SW5kZXg6IDk5OCxcblxuICAvKipcbiAgICogU2NhbGUgKHpvb20gaW4pIHRvIGdpdmVuIHdpZHRoIGFuZCBoZWlnaHQuIElnbm9yZSBzY2FsZUJhc2UgaWYgc2V0LlxuICAgKiBBbHRlcm5hdGl2ZWx5LCBwcm92aWRlIGEgcGVyY2VudGFnZSB2YWx1ZSByZWxhdGl2ZSB0byB0aGUgb3JpZ2luYWwgaW1hZ2Ugc2l6ZS5cbiAgICogQHR5cGUge09iamVjdHxTdHJpbmd9XG4gICAqIEBleGFtcGxlXG4gICAqIGN1c3RvbVNpemU6IHsgd2lkdGg6IDgwMCwgaGVpZ2h0OiA0MDAgfVxuICAgKiBjdXN0b21TaXplOiAxMDAlXG4gICAqL1xuICBjdXN0b21TaXplOiBudWxsLFxuXG4gIC8qKlxuICAgKiBBIGNhbGxiYWNrIGZ1bmN0aW9uIHRoYXQgd2lsbCBiZSBjYWxsZWQgd2hlbiBhIHRhcmdldCBpcyBvcGVuZWQgYW5kXG4gICAqIHRyYW5zaXRpb24gaGFzIGVuZGVkLiBJdCB3aWxsIGdldCB0aGUgdGFyZ2V0IGVsZW1lbnQgYXMgdGhlIGFyZ3VtZW50LlxuICAgKiBAdHlwZSB7RnVuY3Rpb259XG4gICAqL1xuICBvbk9wZW46IG5vb3AsXG5cbiAgLyoqXG4gICAqIFNhbWUgYXMgYWJvdmUsIGV4Y2VwdCBmaXJlZCB3aGVuIGNsb3NlZC5cbiAgICogQHR5cGUge0Z1bmN0aW9ufVxuICAgKi9cbiAgb25DbG9zZTogbm9vcCxcblxuICAvKipcbiAgICogU2FtZSBhcyBhYm92ZSwgZXhjZXB0IGZpcmVkIHdoZW4gZ3JhYmJlZC5cbiAgICogQHR5cGUge0Z1bmN0aW9ufVxuICAgKi9cbiAgb25HcmFiOiBub29wLFxuXG4gIC8qKlxuICAgKiBTYW1lIGFzIGFib3ZlLCBleGNlcHQgZmlyZWQgd2hlbiBtb3ZlZC5cbiAgICogQHR5cGUge0Z1bmN0aW9ufVxuICAgKi9cbiAgb25Nb3ZlOiBub29wLFxuXG4gIC8qKlxuICAgKiBTYW1lIGFzIGFib3ZlLCBleGNlcHQgZmlyZWQgd2hlbiByZWxlYXNlZC5cbiAgICogQHR5cGUge0Z1bmN0aW9ufVxuICAgKi9cbiAgb25SZWxlYXNlOiBub29wLFxuXG4gIC8qKlxuICAgKiBBIGNhbGxiYWNrIGZ1bmN0aW9uIHRoYXQgd2lsbCBiZSBjYWxsZWQgYmVmb3JlIG9wZW4uXG4gICAqIEB0eXBlIHtGdW5jdGlvbn1cbiAgICovXG4gIG9uQmVmb3JlT3Blbjogbm9vcCxcblxuICAvKipcbiAgICogQSBjYWxsYmFjayBmdW5jdGlvbiB0aGF0IHdpbGwgYmUgY2FsbGVkIGJlZm9yZSBjbG9zZS5cbiAgICogQHR5cGUge0Z1bmN0aW9ufVxuICAgKi9cbiAgb25CZWZvcmVDbG9zZTogbm9vcCxcblxuICAvKipcbiAgICogQSBjYWxsYmFjayBmdW5jdGlvbiB0aGF0IHdpbGwgYmUgY2FsbGVkIGJlZm9yZSBncmFiLlxuICAgKiBAdHlwZSB7RnVuY3Rpb259XG4gICAqL1xuICBvbkJlZm9yZUdyYWI6IG5vb3AsXG5cbiAgLyoqXG4gICAqIEEgY2FsbGJhY2sgZnVuY3Rpb24gdGhhdCB3aWxsIGJlIGNhbGxlZCBiZWZvcmUgcmVsZWFzZS5cbiAgICogQHR5cGUge0Z1bmN0aW9ufVxuICAgKi9cbiAgb25CZWZvcmVSZWxlYXNlOiBub29wLFxuXG4gIC8qKlxuICAgKiBBIGNhbGxiYWNrIGZ1bmN0aW9uIHRoYXQgd2lsbCBiZSBjYWxsZWQgd2hlbiB0aGUgaGktcmVzIGltYWdlIGlzIGxvYWRpbmcuXG4gICAqIEB0eXBlIHtGdW5jdGlvbn1cbiAgICovXG4gIG9uSW1hZ2VMb2FkaW5nOiBub29wLFxuXG4gIC8qKlxuICAgKiBBIGNhbGxiYWNrIGZ1bmN0aW9uIHRoYXQgd2lsbCBiZSBjYWxsZWQgd2hlbiB0aGUgaGktcmVzIGltYWdlIGlzIGxvYWRlZC5cbiAgICogQHR5cGUge0Z1bmN0aW9ufVxuICAgKi9cbiAgb25JbWFnZUxvYWRlZDogbm9vcFxufVxuIiwiaW1wb3J0IHsgYmluZEFsbCB9IGZyb20gJy4vdXRpbHMnXG5cbmNvbnN0IFBSRVNTX0RFTEFZID0gMjAwXG5cbmV4cG9ydCBkZWZhdWx0IHtcbiAgaW5pdChpbnN0YW5jZSkge1xuICAgIGJpbmRBbGwodGhpcywgaW5zdGFuY2UpXG4gIH0sXG5cbiAgY2xpY2soZSkge1xuICAgIGUucHJldmVudERlZmF1bHQoKVxuXG4gICAgaWYgKGlzUHJlc3NpbmdNZXRhS2V5KGUpKSB7XG4gICAgICByZXR1cm4gd2luZG93Lm9wZW4oXG4gICAgICAgIHRoaXMudGFyZ2V0LnNyY09yaWdpbmFsIHx8IGUuY3VycmVudFRhcmdldC5zcmMsXG4gICAgICAgICdfYmxhbmsnXG4gICAgICApXG4gICAgfSBlbHNlIHtcbiAgICAgIGlmICh0aGlzLnNob3duKSB7XG4gICAgICAgIGlmICh0aGlzLnJlbGVhc2VkKSB7XG4gICAgICAgICAgdGhpcy5jbG9zZSgpXG4gICAgICAgIH0gZWxzZSB7XG4gICAgICAgICAgdGhpcy5yZWxlYXNlKClcbiAgICAgICAgfVxuICAgICAgfSBlbHNlIHtcbiAgICAgICAgdGhpcy5vcGVuKGUuY3VycmVudFRhcmdldClcbiAgICAgIH1cbiAgICB9XG4gIH0sXG5cbiAgc2Nyb2xsKCkge1xuICAgIGNvbnN0IGVsID1cbiAgICAgIGRvY3VtZW50LmRvY3VtZW50RWxlbWVudCB8fCBkb2N1bWVudC5ib2R5LnBhcmVudE5vZGUgfHwgZG9jdW1lbnQuYm9keVxuICAgIGNvbnN0IHNjcm9sbExlZnQgPSB3aW5kb3cucGFnZVhPZmZzZXQgfHwgZWwuc2Nyb2xsTGVmdFxuICAgIGNvbnN0IHNjcm9sbFRvcCA9IHdpbmRvdy5wYWdlWU9mZnNldCB8fCBlbC5zY3JvbGxUb3BcblxuICAgIGlmICh0aGlzLmxhc3RTY3JvbGxQb3NpdGlvbiA9PT0gbnVsbCkge1xuICAgICAgdGhpcy5sYXN0U2Nyb2xsUG9zaXRpb24gPSB7XG4gICAgICAgIHg6IHNjcm9sbExlZnQsXG4gICAgICAgIHk6IHNjcm9sbFRvcFxuICAgICAgfVxuICAgIH1cblxuICAgIGNvbnN0IGRlbHRhWCA9IHRoaXMubGFzdFNjcm9sbFBvc2l0aW9uLnggLSBzY3JvbGxMZWZ0XG4gICAgY29uc3QgZGVsdGFZID0gdGhpcy5sYXN0U2Nyb2xsUG9zaXRpb24ueSAtIHNjcm9sbFRvcFxuICAgIGNvbnN0IHRocmVzaG9sZCA9IHRoaXMub3B0aW9ucy5zY3JvbGxUaHJlc2hvbGRcblxuICAgIGlmIChNYXRoLmFicyhkZWx0YVkpID49IHRocmVzaG9sZCB8fCBNYXRoLmFicyhkZWx0YVgpID49IHRocmVzaG9sZCkge1xuICAgICAgdGhpcy5sYXN0U2Nyb2xsUG9zaXRpb24gPSBudWxsXG4gICAgICB0aGlzLmNsb3NlKClcbiAgICB9XG4gIH0sXG5cbiAga2V5ZG93bihlKSB7XG4gICAgaWYgKGlzRXNjYXBlKGUpKSB7XG4gICAgICBpZiAodGhpcy5yZWxlYXNlZCkge1xuICAgICAgICB0aGlzLmNsb3NlKClcbiAgICAgIH0gZWxzZSB7XG4gICAgICAgIHRoaXMucmVsZWFzZSh0aGlzLmNsb3NlKVxuICAgICAgfVxuICAgIH1cbiAgfSxcblxuICBtb3VzZWRvd24oZSkge1xuICAgIGlmICghaXNMZWZ0QnV0dG9uKGUpIHx8IGlzUHJlc3NpbmdNZXRhS2V5KGUpKSByZXR1cm5cbiAgICBlLnByZXZlbnREZWZhdWx0KClcbiAgICBjb25zdCB7IGNsaWVudFgsIGNsaWVudFkgfSA9IGVcblxuICAgIHRoaXMucHJlc3NUaW1lciA9IHNldFRpbWVvdXQoXG4gICAgICBmdW5jdGlvbiBncmFiT25Nb3VzZURvd24oKSB7XG4gICAgICAgIHRoaXMuZ3JhYihjbGllbnRYLCBjbGllbnRZKVxuICAgICAgfS5iaW5kKHRoaXMpLFxuICAgICAgUFJFU1NfREVMQVlcbiAgICApXG4gIH0sXG5cbiAgbW91c2Vtb3ZlKGUpIHtcbiAgICBpZiAodGhpcy5yZWxlYXNlZCkgcmV0dXJuXG4gICAgdGhpcy5tb3ZlKGUuY2xpZW50WCwgZS5jbGllbnRZKVxuICB9LFxuXG4gIG1vdXNldXAoZSkge1xuICAgIGlmICghaXNMZWZ0QnV0dG9uKGUpIHx8IGlzUHJlc3NpbmdNZXRhS2V5KGUpKSByZXR1cm5cbiAgICBjbGVhclRpbWVvdXQodGhpcy5wcmVzc1RpbWVyKVxuXG4gICAgaWYgKHRoaXMucmVsZWFzZWQpIHtcbiAgICAgIHRoaXMuY2xvc2UoKVxuICAgIH0gZWxzZSB7XG4gICAgICB0aGlzLnJlbGVhc2UoKVxuICAgIH1cbiAgfSxcblxuICB0b3VjaHN0YXJ0KGUpIHtcbiAgICBlLnByZXZlbnREZWZhdWx0KClcbiAgICBjb25zdCB7IGNsaWVudFgsIGNsaWVudFkgfSA9IGUudG91Y2hlc1swXVxuXG4gICAgdGhpcy5wcmVzc1RpbWVyID0gc2V0VGltZW91dChcbiAgICAgIGZ1bmN0aW9uIGdyYWJPblRvdWNoU3RhcnQoKSB7XG4gICAgICAgIHRoaXMuZ3JhYihjbGllbnRYLCBjbGllbnRZKVxuICAgICAgfS5iaW5kKHRoaXMpLFxuICAgICAgUFJFU1NfREVMQVlcbiAgICApXG4gIH0sXG5cbiAgdG91Y2htb3ZlKGUpIHtcbiAgICBpZiAodGhpcy5yZWxlYXNlZCkgcmV0dXJuXG5cbiAgICBjb25zdCB7IGNsaWVudFgsIGNsaWVudFkgfSA9IGUudG91Y2hlc1swXVxuICAgIHRoaXMubW92ZShjbGllbnRYLCBjbGllbnRZKVxuICB9LFxuXG4gIHRvdWNoZW5kKGUpIHtcbiAgICBpZiAoaXNUb3VjaGluZyhlKSkgcmV0dXJuXG4gICAgY2xlYXJUaW1lb3V0KHRoaXMucHJlc3NUaW1lcilcblxuICAgIGlmICh0aGlzLnJlbGVhc2VkKSB7XG4gICAgICB0aGlzLmNsb3NlKClcbiAgICB9IGVsc2Uge1xuICAgICAgdGhpcy5yZWxlYXNlKClcbiAgICB9XG4gIH0sXG5cbiAgY2xpY2tPdmVybGF5KCkge1xuICAgIHRoaXMuY2xvc2UoKVxuICB9LFxuXG4gIHJlc2l6ZVdpbmRvdygpIHtcbiAgICB0aGlzLmNsb3NlKClcbiAgfVxufVxuXG5mdW5jdGlvbiBpc0xlZnRCdXR0b24oZSkge1xuICByZXR1cm4gZS5idXR0b24gPT09IDBcbn1cblxuZnVuY3Rpb24gaXNQcmVzc2luZ01ldGFLZXkoZSkge1xuICByZXR1cm4gZS5tZXRhS2V5IHx8IGUuY3RybEtleVxufVxuXG5mdW5jdGlvbiBpc1RvdWNoaW5nKGUpIHtcbiAgZS50YXJnZXRUb3VjaGVzLmxlbmd0aCA+IDBcbn1cblxuZnVuY3Rpb24gaXNFc2NhcGUoZSkge1xuICBjb25zdCBjb2RlID0gZS5rZXkgfHwgZS5jb2RlXG4gIHJldHVybiBjb2RlID09PSAnRXNjYXBlJyB8fCBlLmtleUNvZGUgPT09IDI3XG59XG4iLCJpbXBvcnQgeyBsaXN0ZW4sIHNldFN0eWxlIH0gZnJvbSAnLi91dGlscydcblxuZXhwb3J0IGRlZmF1bHQge1xuICBpbml0KGluc3RhbmNlKSB7XG4gICAgdGhpcy5lbCA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoJ2RpdicpXG4gICAgdGhpcy5pbnN0YW5jZSA9IGluc3RhbmNlXG4gICAgdGhpcy5wYXJlbnQgPSBkb2N1bWVudC5ib2R5XG5cbiAgICBzZXRTdHlsZSh0aGlzLmVsLCB7XG4gICAgICBwb3NpdGlvbjogJ2ZpeGVkJyxcbiAgICAgIHRvcDogMCxcbiAgICAgIGxlZnQ6IDAsXG4gICAgICByaWdodDogMCxcbiAgICAgIGJvdHRvbTogMCxcbiAgICAgIG9wYWNpdHk6IDBcbiAgICB9KVxuXG4gICAgdGhpcy51cGRhdGVTdHlsZShpbnN0YW5jZS5vcHRpb25zKVxuICAgIGxpc3Rlbih0aGlzLmVsLCAnY2xpY2snLCBpbnN0YW5jZS5oYW5kbGVyLmNsaWNrT3ZlcmxheS5iaW5kKGluc3RhbmNlKSlcbiAgfSxcblxuICB1cGRhdGVTdHlsZShvcHRpb25zKSB7XG4gICAgc2V0U3R5bGUodGhpcy5lbCwge1xuICAgICAgekluZGV4OiBvcHRpb25zLnpJbmRleCxcbiAgICAgIGJhY2tncm91bmRDb2xvcjogb3B0aW9ucy5iZ0NvbG9yLFxuICAgICAgdHJhbnNpdGlvbjogYG9wYWNpdHlcbiAgICAgICAgJHtvcHRpb25zLnRyYW5zaXRpb25EdXJhdGlvbn1zXG4gICAgICAgICR7b3B0aW9ucy50cmFuc2l0aW9uVGltaW5nRnVuY3Rpb259YFxuICAgIH0pXG4gIH0sXG5cbiAgaW5zZXJ0KCkge1xuICAgIHRoaXMucGFyZW50LmFwcGVuZENoaWxkKHRoaXMuZWwpXG4gIH0sXG5cbiAgcmVtb3ZlKCkge1xuICAgIHRoaXMucGFyZW50LnJlbW92ZUNoaWxkKHRoaXMuZWwpXG4gIH0sXG5cbiAgZmFkZUluKCkge1xuICAgIHRoaXMuZWwub2Zmc2V0V2lkdGhcbiAgICB0aGlzLmVsLnN0eWxlLm9wYWNpdHkgPSB0aGlzLmluc3RhbmNlLm9wdGlvbnMuYmdPcGFjaXR5XG4gIH0sXG5cbiAgZmFkZU91dCgpIHtcbiAgICB0aGlzLmVsLnN0eWxlLm9wYWNpdHkgPSAwXG4gIH1cbn1cbiIsImltcG9ydCB7IGN1cnNvciwgc2V0U3R5bGUsIGdldE9yaWdpbmFsU291cmNlIH0gZnJvbSAnLi91dGlscydcblxuLy8gVHJhbnNsYXRlIHotYXhpcyB0byBmaXggQ1NTIGdyaWQgZGlzcGxheSBpc3N1ZSBpbiBDaHJvbWU6XG4vLyBodHRwczovL2dpdGh1Yi5jb20va2luZ2RpZG85OTkvem9vbWluZy9pc3N1ZXMvNDJcbmNvbnN0IFRSQU5TTEFURV9aID0gMFxuXG5leHBvcnQgZGVmYXVsdCB7XG4gIGluaXQoZWwsIGluc3RhbmNlKSB7XG4gICAgdGhpcy5lbCA9IGVsXG4gICAgdGhpcy5pbnN0YW5jZSA9IGluc3RhbmNlXG4gICAgdGhpcy5zcmNUaHVtYm5haWwgPSB0aGlzLmVsLmdldEF0dHJpYnV0ZSgnc3JjJylcbiAgICB0aGlzLnNyY3NldCA9IHRoaXMuZWwuZ2V0QXR0cmlidXRlKCdzcmNzZXQnKVxuICAgIHRoaXMuc3JjT3JpZ2luYWwgPSBnZXRPcmlnaW5hbFNvdXJjZSh0aGlzLmVsKVxuICAgIHRoaXMucmVjdCA9IHRoaXMuZWwuZ2V0Qm91bmRpbmdDbGllbnRSZWN0KClcbiAgICB0aGlzLnRyYW5zbGF0ZSA9IG51bGxcbiAgICB0aGlzLnNjYWxlID0gbnVsbFxuICAgIHRoaXMuc3R5bGVPcGVuID0gbnVsbFxuICAgIHRoaXMuc3R5bGVDbG9zZSA9IG51bGxcbiAgfSxcblxuICB6b29tSW4oKSB7XG4gICAgY29uc3Qge1xuICAgICAgekluZGV4LFxuICAgICAgZW5hYmxlR3JhYixcbiAgICAgIHRyYW5zaXRpb25EdXJhdGlvbixcbiAgICAgIHRyYW5zaXRpb25UaW1pbmdGdW5jdGlvblxuICAgIH0gPSB0aGlzLmluc3RhbmNlLm9wdGlvbnNcbiAgICB0aGlzLnRyYW5zbGF0ZSA9IHRoaXMuY2FsY3VsYXRlVHJhbnNsYXRlKClcbiAgICB0aGlzLnNjYWxlID0gdGhpcy5jYWxjdWxhdGVTY2FsZSgpXG5cbiAgICB0aGlzLnN0eWxlT3BlbiA9IHtcbiAgICAgIHBvc2l0aW9uOiAncmVsYXRpdmUnLFxuICAgICAgekluZGV4OiB6SW5kZXggKyAxLFxuICAgICAgY3Vyc29yOiBlbmFibGVHcmFiID8gY3Vyc29yLmdyYWIgOiBjdXJzb3Iuem9vbU91dCxcbiAgICAgIHRyYW5zaXRpb246IGB0cmFuc2Zvcm1cbiAgICAgICAgJHt0cmFuc2l0aW9uRHVyYXRpb259c1xuICAgICAgICAke3RyYW5zaXRpb25UaW1pbmdGdW5jdGlvbn1gLFxuICAgICAgdHJhbnNmb3JtOiBgdHJhbnNsYXRlM2QoJHt0aGlzLnRyYW5zbGF0ZS54fXB4LCAke1xuICAgICAgICB0aGlzLnRyYW5zbGF0ZS55XG4gICAgICB9cHgsICR7VFJBTlNMQVRFX1p9cHgpXG4gICAgICAgIHNjYWxlKCR7dGhpcy5zY2FsZS54fSwke3RoaXMuc2NhbGUueX0pYCxcbiAgICAgIGhlaWdodDogYCR7dGhpcy5yZWN0LmhlaWdodH1weGAsXG4gICAgICB3aWR0aDogYCR7dGhpcy5yZWN0LndpZHRofXB4YFxuICAgIH1cblxuICAgIC8vIEZvcmNlIGxheW91dCB1cGRhdGVcbiAgICB0aGlzLmVsLm9mZnNldFdpZHRoXG5cbiAgICAvLyBUcmlnZ2VyIHRyYW5zaXRpb25cbiAgICB0aGlzLnN0eWxlQ2xvc2UgPSBzZXRTdHlsZSh0aGlzLmVsLCB0aGlzLnN0eWxlT3BlbiwgdHJ1ZSlcbiAgfSxcblxuICB6b29tT3V0KCkge1xuICAgIC8vIEZvcmNlIGxheW91dCB1cGRhdGVcbiAgICB0aGlzLmVsLm9mZnNldFdpZHRoXG5cbiAgICBzZXRTdHlsZSh0aGlzLmVsLCB7IHRyYW5zZm9ybTogJ25vbmUnIH0pXG4gIH0sXG5cbiAgZ3JhYih4LCB5LCBzY2FsZUV4dHJhKSB7XG4gICAgY29uc3Qgd2luZG93Q2VudGVyID0gZ2V0V2luZG93Q2VudGVyKClcbiAgICBjb25zdCBbZHgsIGR5XSA9IFt3aW5kb3dDZW50ZXIueCAtIHgsIHdpbmRvd0NlbnRlci55IC0geV1cblxuICAgIHNldFN0eWxlKHRoaXMuZWwsIHtcbiAgICAgIGN1cnNvcjogY3Vyc29yLm1vdmUsXG4gICAgICB0cmFuc2Zvcm06IGB0cmFuc2xhdGUzZChcbiAgICAgICAgJHt0aGlzLnRyYW5zbGF0ZS54ICsgZHh9cHgsICR7dGhpcy50cmFuc2xhdGUueSArXG4gICAgICAgIGR5fXB4LCAke1RSQU5TTEFURV9afXB4KVxuICAgICAgICBzY2FsZSgke3RoaXMuc2NhbGUueCArIHNjYWxlRXh0cmF9LCR7dGhpcy5zY2FsZS55ICsgc2NhbGVFeHRyYX0pYFxuICAgIH0pXG4gIH0sXG5cbiAgbW92ZSh4LCB5LCBzY2FsZUV4dHJhKSB7XG4gICAgY29uc3Qgd2luZG93Q2VudGVyID0gZ2V0V2luZG93Q2VudGVyKClcbiAgICBjb25zdCBbZHgsIGR5XSA9IFt3aW5kb3dDZW50ZXIueCAtIHgsIHdpbmRvd0NlbnRlci55IC0geV1cblxuICAgIHNldFN0eWxlKHRoaXMuZWwsIHtcbiAgICAgIHRyYW5zaXRpb246ICd0cmFuc2Zvcm0nLFxuICAgICAgdHJhbnNmb3JtOiBgdHJhbnNsYXRlM2QoXG4gICAgICAgICR7dGhpcy50cmFuc2xhdGUueCArIGR4fXB4LCAke3RoaXMudHJhbnNsYXRlLnkgK1xuICAgICAgICBkeX1weCwgJHtUUkFOU0xBVEVfWn1weClcbiAgICAgICAgc2NhbGUoJHt0aGlzLnNjYWxlLnggKyBzY2FsZUV4dHJhfSwke3RoaXMuc2NhbGUueSArIHNjYWxlRXh0cmF9KWBcbiAgICB9KVxuICB9LFxuXG4gIHJlc3RvcmVDbG9zZVN0eWxlKCkge1xuICAgIHNldFN0eWxlKHRoaXMuZWwsIHRoaXMuc3R5bGVDbG9zZSlcbiAgfSxcblxuICByZXN0b3JlT3BlblN0eWxlKCkge1xuICAgIHNldFN0eWxlKHRoaXMuZWwsIHRoaXMuc3R5bGVPcGVuKVxuICB9LFxuXG4gIHVwZ3JhZGVTb3VyY2UoKSB7XG4gICAgaWYgKHRoaXMuc3JjT3JpZ2luYWwpIHtcbiAgICAgIGNvbnN0IHBhcmVudE5vZGUgPSB0aGlzLmVsLnBhcmVudE5vZGVcblxuICAgICAgaWYgKHRoaXMuc3Jjc2V0KSB7XG4gICAgICAgIHRoaXMuZWwucmVtb3ZlQXR0cmlidXRlKCdzcmNzZXQnKVxuICAgICAgfVxuXG4gICAgICBjb25zdCB0ZW1wID0gdGhpcy5lbC5jbG9uZU5vZGUoZmFsc2UpXG5cbiAgICAgIC8vIEZvcmNlIGNvbXB1dGUgdGhlIGhpLXJlcyBpbWFnZSBpbiBET00gdG8gcHJldmVudFxuICAgICAgLy8gaW1hZ2UgZmxpY2tlcmluZyB3aGlsZSB1cGRhdGluZyBzcmNcbiAgICAgIHRlbXAuc2V0QXR0cmlidXRlKCdzcmMnLCB0aGlzLnNyY09yaWdpbmFsKVxuICAgICAgdGVtcC5zdHlsZS5wb3NpdGlvbiA9ICdmaXhlZCdcbiAgICAgIHRlbXAuc3R5bGUudmlzaWJpbGl0eSA9ICdoaWRkZW4nXG4gICAgICBwYXJlbnROb2RlLmFwcGVuZENoaWxkKHRlbXApXG5cbiAgICAgIC8vIEFkZCBkZWxheSB0byBwcmV2ZW50IEZpcmVmb3ggZnJvbSBmbGlja2VyaW5nXG4gICAgICBzZXRUaW1lb3V0KFxuICAgICAgICBmdW5jdGlvbiB1cGRhdGVTcmMoKSB7XG4gICAgICAgICAgdGhpcy5lbC5zZXRBdHRyaWJ1dGUoJ3NyYycsIHRoaXMuc3JjT3JpZ2luYWwpXG4gICAgICAgICAgcGFyZW50Tm9kZS5yZW1vdmVDaGlsZCh0ZW1wKVxuICAgICAgICB9LmJpbmQodGhpcyksXG4gICAgICAgIDUwXG4gICAgICApXG4gICAgfVxuICB9LFxuXG4gIGRvd25ncmFkZVNvdXJjZSgpIHtcbiAgICBpZiAodGhpcy5zcmNPcmlnaW5hbCkge1xuICAgICAgaWYgKHRoaXMuc3Jjc2V0KSB7XG4gICAgICAgIHRoaXMuZWwuc2V0QXR0cmlidXRlKCdzcmNzZXQnLCB0aGlzLnNyY3NldClcbiAgICAgIH1cbiAgICAgIHRoaXMuZWwuc2V0QXR0cmlidXRlKCdzcmMnLCB0aGlzLnNyY1RodW1ibmFpbClcbiAgICB9XG4gIH0sXG5cbiAgY2FsY3VsYXRlVHJhbnNsYXRlKCkge1xuICAgIGNvbnN0IHdpbmRvd0NlbnRlciA9IGdldFdpbmRvd0NlbnRlcigpXG4gICAgY29uc3QgdGFyZ2V0Q2VudGVyID0ge1xuICAgICAgeDogdGhpcy5yZWN0LmxlZnQgKyB0aGlzLnJlY3Qud2lkdGggLyAyLFxuICAgICAgeTogdGhpcy5yZWN0LnRvcCArIHRoaXMucmVjdC5oZWlnaHQgLyAyXG4gICAgfVxuXG4gICAgLy8gVGhlIHZlY3RvciB0byB0cmFuc2xhdGUgaW1hZ2UgdG8gdGhlIHdpbmRvdyBjZW50ZXJcbiAgICByZXR1cm4ge1xuICAgICAgeDogd2luZG93Q2VudGVyLnggLSB0YXJnZXRDZW50ZXIueCxcbiAgICAgIHk6IHdpbmRvd0NlbnRlci55IC0gdGFyZ2V0Q2VudGVyLnlcbiAgICB9XG4gIH0sXG5cbiAgY2FsY3VsYXRlU2NhbGUoKSB7XG4gICAgY29uc3QgeyB6b29taW5nSGVpZ2h0LCB6b29taW5nV2lkdGggfSA9IHRoaXMuZWwuZGF0YXNldFxuICAgIGNvbnN0IHsgY3VzdG9tU2l6ZSwgc2NhbGVCYXNlIH0gPSB0aGlzLmluc3RhbmNlLm9wdGlvbnNcblxuICAgIGlmICghY3VzdG9tU2l6ZSAmJiB6b29taW5nSGVpZ2h0ICYmIHpvb21pbmdXaWR0aCkge1xuICAgICAgcmV0dXJuIHtcbiAgICAgICAgeDogem9vbWluZ1dpZHRoIC8gdGhpcy5yZWN0LndpZHRoLFxuICAgICAgICB5OiB6b29taW5nSGVpZ2h0IC8gdGhpcy5yZWN0LmhlaWdodFxuICAgICAgfVxuICAgIH0gZWxzZSBpZiAoY3VzdG9tU2l6ZSAmJiB0eXBlb2YgY3VzdG9tU2l6ZSA9PT0gJ29iamVjdCcpIHtcbiAgICAgIHJldHVybiB7XG4gICAgICAgIHg6IGN1c3RvbVNpemUud2lkdGggLyB0aGlzLnJlY3Qud2lkdGgsXG4gICAgICAgIHk6IGN1c3RvbVNpemUuaGVpZ2h0IC8gdGhpcy5yZWN0LmhlaWdodFxuICAgICAgfVxuICAgIH0gZWxzZSB7XG4gICAgICBjb25zdCB0YXJnZXRIYWxmV2lkdGggPSB0aGlzLnJlY3Qud2lkdGggLyAyXG4gICAgICBjb25zdCB0YXJnZXRIYWxmSGVpZ2h0ID0gdGhpcy5yZWN0LmhlaWdodCAvIDJcbiAgICAgIGNvbnN0IHdpbmRvd0NlbnRlciA9IGdldFdpbmRvd0NlbnRlcigpXG5cbiAgICAgIC8vIFRoZSBkaXN0YW5jZSBiZXR3ZWVuIHRhcmdldCBlZGdlIGFuZCB3aW5kb3cgZWRnZVxuICAgICAgY29uc3QgdGFyZ2V0RWRnZVRvV2luZG93RWRnZSA9IHtcbiAgICAgICAgeDogd2luZG93Q2VudGVyLnggLSB0YXJnZXRIYWxmV2lkdGgsXG4gICAgICAgIHk6IHdpbmRvd0NlbnRlci55IC0gdGFyZ2V0SGFsZkhlaWdodFxuICAgICAgfVxuXG4gICAgICBjb25zdCBzY2FsZUhvcml6b250YWxseSA9IHRhcmdldEVkZ2VUb1dpbmRvd0VkZ2UueCAvIHRhcmdldEhhbGZXaWR0aFxuICAgICAgY29uc3Qgc2NhbGVWZXJ0aWNhbGx5ID0gdGFyZ2V0RWRnZVRvV2luZG93RWRnZS55IC8gdGFyZ2V0SGFsZkhlaWdodFxuXG4gICAgICAvLyBUaGUgYWRkaXRpb25hbCBzY2FsZSBpcyBiYXNlZCBvbiB0aGUgc21hbGxlciB2YWx1ZSBvZlxuICAgICAgLy8gc2NhbGluZyBob3Jpem9udGFsbHkgYW5kIHNjYWxpbmcgdmVydGljYWxseVxuICAgICAgY29uc3Qgc2NhbGUgPSBzY2FsZUJhc2UgKyBNYXRoLm1pbihzY2FsZUhvcml6b250YWxseSwgc2NhbGVWZXJ0aWNhbGx5KVxuXG4gICAgICBpZiAoY3VzdG9tU2l6ZSAmJiB0eXBlb2YgY3VzdG9tU2l6ZSA9PT0gJ3N0cmluZycpIHtcbiAgICAgICAgLy8gVXNlIHpvb21pbmdXaWR0aCBhbmQgem9vbWluZ0hlaWdodCBpZiBhdmFpbGFibGVcbiAgICAgICAgY29uc3QgbmF0dXJhbFdpZHRoID0gem9vbWluZ1dpZHRoIHx8IHRoaXMuZWwubmF0dXJhbFdpZHRoXG4gICAgICAgIGNvbnN0IG5hdHVyYWxIZWlnaHQgPSB6b29taW5nSGVpZ2h0IHx8IHRoaXMuZWwubmF0dXJhbEhlaWdodFxuICAgICAgICBjb25zdCBtYXhab29taW5nV2lkdGggPVxuICAgICAgICAgIHBhcnNlRmxvYXQoY3VzdG9tU2l6ZSkgKiBuYXR1cmFsV2lkdGggLyAoMTAwICogdGhpcy5yZWN0LndpZHRoKVxuICAgICAgICBjb25zdCBtYXhab29taW5nSGVpZ2h0ID1cbiAgICAgICAgICBwYXJzZUZsb2F0KGN1c3RvbVNpemUpICogbmF0dXJhbEhlaWdodCAvICgxMDAgKiB0aGlzLnJlY3QuaGVpZ2h0KVxuXG4gICAgICAgIC8vIE9ubHkgc2NhbGUgaW1hZ2UgdXAgdG8gdGhlIHNwZWNpZmllZCBjdXN0b21TaXplIHBlcmNlbnRhZ2VcbiAgICAgICAgaWYgKHNjYWxlID4gbWF4Wm9vbWluZ1dpZHRoIHx8IHNjYWxlID4gbWF4Wm9vbWluZ0hlaWdodCkge1xuICAgICAgICAgIHJldHVybiB7XG4gICAgICAgICAgICB4OiBtYXhab29taW5nV2lkdGgsXG4gICAgICAgICAgICB5OiBtYXhab29taW5nSGVpZ2h0XG4gICAgICAgICAgfVxuICAgICAgICB9XG4gICAgICB9XG5cbiAgICAgIHJldHVybiB7XG4gICAgICAgIHg6IHNjYWxlLFxuICAgICAgICB5OiBzY2FsZVxuICAgICAgfVxuICAgIH1cbiAgfVxufVxuXG5mdW5jdGlvbiBnZXRXaW5kb3dDZW50ZXIoKSB7XG4gIGNvbnN0IGRvY0VsID0gZG9jdW1lbnQuZG9jdW1lbnRFbGVtZW50XG4gIGNvbnN0IHdpbmRvd1dpZHRoID0gTWF0aC5taW4oZG9jRWwuY2xpZW50V2lkdGgsIHdpbmRvdy5pbm5lcldpZHRoKVxuICBjb25zdCB3aW5kb3dIZWlnaHQgPSBNYXRoLm1pbihkb2NFbC5jbGllbnRIZWlnaHQsIHdpbmRvdy5pbm5lckhlaWdodClcblxuICByZXR1cm4ge1xuICAgIHg6IHdpbmRvd1dpZHRoIC8gMixcbiAgICB5OiB3aW5kb3dIZWlnaHQgLyAyXG4gIH1cbn1cbiIsImltcG9ydCB7IGN1cnNvciwgbGlzdGVuLCBsb2FkSW1hZ2UsIGdldE9yaWdpbmFsU291cmNlIH0gZnJvbSAnLi91dGlscydcbmltcG9ydCBERUZBVUxUX09QVElPTlMgZnJvbSAnLi9vcHRpb25zJ1xuXG5pbXBvcnQgaGFuZGxlciBmcm9tICcuL2hhbmRsZXInXG5pbXBvcnQgb3ZlcmxheSBmcm9tICcuL292ZXJsYXknXG5pbXBvcnQgdGFyZ2V0IGZyb20gJy4vdGFyZ2V0J1xuXG4vKipcbiAqIFpvb21pbmcgaW5zdGFuY2UuXG4gKi9cbmV4cG9ydCBkZWZhdWx0IGNsYXNzIFpvb21pbmcge1xuICAvKipcbiAgICogQHBhcmFtIHtPYmplY3R9IFtvcHRpb25zXSBVcGRhdGUgZGVmYXVsdCBvcHRpb25zIGlmIHByb3ZpZGVkLlxuICAgKi9cbiAgY29uc3RydWN0b3Iob3B0aW9ucykge1xuICAgIHRoaXMudGFyZ2V0ID0gT2JqZWN0LmNyZWF0ZSh0YXJnZXQpXG4gICAgdGhpcy5vdmVybGF5ID0gT2JqZWN0LmNyZWF0ZShvdmVybGF5KVxuICAgIHRoaXMuaGFuZGxlciA9IE9iamVjdC5jcmVhdGUoaGFuZGxlcilcbiAgICB0aGlzLmJvZHkgPSBkb2N1bWVudC5ib2R5XG5cbiAgICB0aGlzLnNob3duID0gZmFsc2VcbiAgICB0aGlzLmxvY2sgPSBmYWxzZVxuICAgIHRoaXMucmVsZWFzZWQgPSB0cnVlXG4gICAgdGhpcy5sYXN0U2Nyb2xsUG9zaXRpb24gPSBudWxsXG4gICAgdGhpcy5wcmVzc1RpbWVyID0gbnVsbFxuXG4gICAgdGhpcy5vcHRpb25zID0gT2JqZWN0LmFzc2lnbih7fSwgREVGQVVMVF9PUFRJT05TLCBvcHRpb25zKVxuICAgIHRoaXMub3ZlcmxheS5pbml0KHRoaXMpXG4gICAgdGhpcy5oYW5kbGVyLmluaXQodGhpcylcbiAgfVxuXG4gIC8qKlxuICAgKiBNYWtlIGVsZW1lbnQocykgem9vbWFibGUuXG4gICAqIEBwYXJhbSAge3N0cmluZ3xFbGVtZW50fSBlbCBBIGNzcyBzZWxlY3RvciBvciBhbiBFbGVtZW50LlxuICAgKiBAcmV0dXJuIHt0aGlzfVxuICAgKi9cbiAgbGlzdGVuKGVsKSB7XG4gICAgaWYgKHR5cGVvZiBlbCA9PT0gJ3N0cmluZycpIHtcbiAgICAgIGNvbnN0IGVscyA9IGRvY3VtZW50LnF1ZXJ5U2VsZWN0b3JBbGwoZWwpXG4gICAgICBsZXQgaSA9IGVscy5sZW5ndGhcblxuICAgICAgd2hpbGUgKGktLSkge1xuICAgICAgICB0aGlzLmxpc3RlbihlbHNbaV0pXG4gICAgICB9XG4gICAgfSBlbHNlIGlmIChlbC50YWdOYW1lID09PSAnSU1HJykge1xuICAgICAgZWwuc3R5bGUuY3Vyc29yID0gY3Vyc29yLnpvb21JblxuICAgICAgbGlzdGVuKGVsLCAnY2xpY2snLCB0aGlzLmhhbmRsZXIuY2xpY2spXG5cbiAgICAgIGlmICh0aGlzLm9wdGlvbnMucHJlbG9hZEltYWdlKSB7XG4gICAgICAgIGxvYWRJbWFnZShnZXRPcmlnaW5hbFNvdXJjZShlbCkpXG4gICAgICB9XG4gICAgfVxuXG4gICAgcmV0dXJuIHRoaXNcbiAgfVxuXG4gIC8qKlxuICAgKiBVcGRhdGUgb3B0aW9ucyBvciByZXR1cm4gY3VycmVudCBvcHRpb25zIGlmIG5vIGFyZ3VtZW50IGlzIHByb3ZpZGVkLlxuICAgKiBAcGFyYW0gIHtPYmplY3R9IG9wdGlvbnMgQW4gT2JqZWN0IHRoYXQgY29udGFpbnMgdGhpcy5vcHRpb25zLlxuICAgKiBAcmV0dXJuIHt0aGlzfHRoaXMub3B0aW9uc31cbiAgICovXG4gIGNvbmZpZyhvcHRpb25zKSB7XG4gICAgaWYgKG9wdGlvbnMpIHtcbiAgICAgIE9iamVjdC5hc3NpZ24odGhpcy5vcHRpb25zLCBvcHRpb25zKVxuICAgICAgdGhpcy5vdmVybGF5LnVwZGF0ZVN0eWxlKHRoaXMub3B0aW9ucylcbiAgICAgIHJldHVybiB0aGlzXG4gICAgfSBlbHNlIHtcbiAgICAgIHJldHVybiB0aGlzLm9wdGlvbnNcbiAgICB9XG4gIH1cblxuICAvKipcbiAgICogT3BlbiAoem9vbSBpbikgdGhlIEVsZW1lbnQuXG4gICAqIEBwYXJhbSAge0VsZW1lbnR9IGVsIFRoZSBFbGVtZW50IHRvIG9wZW4uXG4gICAqIEBwYXJhbSAge0Z1bmN0aW9ufSBbY2I9dGhpcy5vcHRpb25zLm9uT3Blbl0gQSBjYWxsYmFjayBmdW5jdGlvbiB0aGF0IHdpbGxcbiAgICogYmUgY2FsbGVkIHdoZW4gYSB0YXJnZXQgaXMgb3BlbmVkIGFuZCB0cmFuc2l0aW9uIGhhcyBlbmRlZC4gSXQgd2lsbCBnZXRcbiAgICogdGhlIHRhcmdldCBlbGVtZW50IGFzIHRoZSBhcmd1bWVudC5cbiAgICogQHJldHVybiB7dGhpc31cbiAgICovXG4gIG9wZW4oZWwsIGNiID0gdGhpcy5vcHRpb25zLm9uT3Blbikge1xuICAgIGlmICh0aGlzLnNob3duIHx8IHRoaXMubG9jaykgcmV0dXJuXG5cbiAgICBjb25zdCB0YXJnZXQgPSB0eXBlb2YgZWwgPT09ICdzdHJpbmcnID8gZG9jdW1lbnQucXVlcnlTZWxlY3RvcihlbCkgOiBlbFxuXG4gICAgaWYgKHRhcmdldC50YWdOYW1lICE9PSAnSU1HJykgcmV0dXJuXG5cbiAgICB0aGlzLm9wdGlvbnMub25CZWZvcmVPcGVuKHRhcmdldClcblxuICAgIHRoaXMudGFyZ2V0LmluaXQodGFyZ2V0LCB0aGlzKVxuXG4gICAgaWYgKCF0aGlzLm9wdGlvbnMucHJlbG9hZEltYWdlKSB7XG4gICAgICBjb25zdCB7IHNyY09yaWdpbmFsIH0gPSB0aGlzLnRhcmdldFxuXG4gICAgICBpZiAoc3JjT3JpZ2luYWwgIT0gbnVsbCkge1xuICAgICAgICB0aGlzLm9wdGlvbnMub25JbWFnZUxvYWRpbmcodGFyZ2V0KVxuICAgICAgICBsb2FkSW1hZ2Uoc3JjT3JpZ2luYWwsIHRoaXMub3B0aW9ucy5vbkltYWdlTG9hZGVkKVxuICAgICAgfVxuICAgIH1cblxuICAgIHRoaXMuc2hvd24gPSB0cnVlXG4gICAgdGhpcy5sb2NrID0gdHJ1ZVxuXG4gICAgdGhpcy50YXJnZXQuem9vbUluKClcbiAgICB0aGlzLm92ZXJsYXkuaW5zZXJ0KClcbiAgICB0aGlzLm92ZXJsYXkuZmFkZUluKClcblxuICAgIGxpc3Rlbihkb2N1bWVudCwgJ3Njcm9sbCcsIHRoaXMuaGFuZGxlci5zY3JvbGwpXG4gICAgbGlzdGVuKGRvY3VtZW50LCAna2V5ZG93bicsIHRoaXMuaGFuZGxlci5rZXlkb3duKVxuXG4gICAgaWYgKHRoaXMub3B0aW9ucy5jbG9zZU9uV2luZG93UmVzaXplKSB7XG4gICAgICBsaXN0ZW4od2luZG93LCAncmVzaXplJywgdGhpcy5oYW5kbGVyLnJlc2l6ZVdpbmRvdylcbiAgICB9XG5cbiAgICBjb25zdCBvbk9wZW5FbmQgPSAoKSA9PiB7XG4gICAgICBsaXN0ZW4odGFyZ2V0LCAndHJhbnNpdGlvbmVuZCcsIG9uT3BlbkVuZCwgZmFsc2UpXG4gICAgICB0aGlzLmxvY2sgPSBmYWxzZVxuICAgICAgdGhpcy50YXJnZXQudXBncmFkZVNvdXJjZSgpXG5cbiAgICAgIGlmICh0aGlzLm9wdGlvbnMuZW5hYmxlR3JhYikge1xuICAgICAgICB0b2dnbGVHcmFiTGlzdGVuZXJzKGRvY3VtZW50LCB0aGlzLmhhbmRsZXIsIHRydWUpXG4gICAgICB9XG5cbiAgICAgIGNiKHRhcmdldClcbiAgICB9XG5cbiAgICBsaXN0ZW4odGFyZ2V0LCAndHJhbnNpdGlvbmVuZCcsIG9uT3BlbkVuZClcblxuICAgIHJldHVybiB0aGlzXG4gIH1cblxuICAvKipcbiAgICogQ2xvc2UgKHpvb20gb3V0KSB0aGUgRWxlbWVudCBjdXJyZW50bHkgb3BlbmVkLlxuICAgKiBAcGFyYW0gIHtGdW5jdGlvbn0gW2NiPXRoaXMub3B0aW9ucy5vbkNsb3NlXSBBIGNhbGxiYWNrIGZ1bmN0aW9uIHRoYXQgd2lsbFxuICAgKiBiZSBjYWxsZWQgd2hlbiBhIHRhcmdldCBpcyBjbG9zZWQgYW5kIHRyYW5zaXRpb24gaGFzIGVuZGVkLiBJdCB3aWxsIGdldFxuICAgKiB0aGUgdGFyZ2V0IGVsZW1lbnQgYXMgdGhlIGFyZ3VtZW50LlxuICAgKiBAcmV0dXJuIHt0aGlzfVxuICAgKi9cbiAgY2xvc2UoY2IgPSB0aGlzLm9wdGlvbnMub25DbG9zZSkge1xuICAgIGlmICghdGhpcy5zaG93biB8fCB0aGlzLmxvY2spIHJldHVyblxuXG4gICAgY29uc3QgdGFyZ2V0ID0gdGhpcy50YXJnZXQuZWxcblxuICAgIHRoaXMub3B0aW9ucy5vbkJlZm9yZUNsb3NlKHRhcmdldClcblxuICAgIHRoaXMubG9jayA9IHRydWVcbiAgICB0aGlzLmJvZHkuc3R5bGUuY3Vyc29yID0gY3Vyc29yLmRlZmF1bHRcbiAgICB0aGlzLm92ZXJsYXkuZmFkZU91dCgpXG4gICAgdGhpcy50YXJnZXQuem9vbU91dCgpXG5cbiAgICBsaXN0ZW4oZG9jdW1lbnQsICdzY3JvbGwnLCB0aGlzLmhhbmRsZXIuc2Nyb2xsLCBmYWxzZSlcbiAgICBsaXN0ZW4oZG9jdW1lbnQsICdrZXlkb3duJywgdGhpcy5oYW5kbGVyLmtleWRvd24sIGZhbHNlKVxuXG4gICAgaWYgKHRoaXMub3B0aW9ucy5jbG9zZU9uV2luZG93UmVzaXplKSB7XG4gICAgICBsaXN0ZW4od2luZG93LCAncmVzaXplJywgdGhpcy5oYW5kbGVyLnJlc2l6ZVdpbmRvdywgZmFsc2UpXG4gICAgfVxuXG4gICAgY29uc3Qgb25DbG9zZUVuZCA9ICgpID0+IHtcbiAgICAgIGxpc3Rlbih0YXJnZXQsICd0cmFuc2l0aW9uZW5kJywgb25DbG9zZUVuZCwgZmFsc2UpXG5cbiAgICAgIHRoaXMuc2hvd24gPSBmYWxzZVxuICAgICAgdGhpcy5sb2NrID0gZmFsc2VcblxuICAgICAgdGhpcy50YXJnZXQuZG93bmdyYWRlU291cmNlKClcblxuICAgICAgaWYgKHRoaXMub3B0aW9ucy5lbmFibGVHcmFiKSB7XG4gICAgICAgIHRvZ2dsZUdyYWJMaXN0ZW5lcnMoZG9jdW1lbnQsIHRoaXMuaGFuZGxlciwgZmFsc2UpXG4gICAgICB9XG5cbiAgICAgIHRoaXMudGFyZ2V0LnJlc3RvcmVDbG9zZVN0eWxlKClcbiAgICAgIHRoaXMub3ZlcmxheS5yZW1vdmUoKVxuXG4gICAgICBjYih0YXJnZXQpXG4gICAgfVxuXG4gICAgbGlzdGVuKHRhcmdldCwgJ3RyYW5zaXRpb25lbmQnLCBvbkNsb3NlRW5kKVxuXG4gICAgcmV0dXJuIHRoaXNcbiAgfVxuXG4gIC8qKlxuICAgKiBHcmFiIHRoZSBFbGVtZW50IGN1cnJlbnRseSBvcGVuZWQgZ2l2ZW4gYSBwb3NpdGlvbiBhbmQgYXBwbHkgZXh0cmEgem9vbS1pbi5cbiAgICogQHBhcmFtICB7bnVtYmVyfSAgIHggVGhlIFgtYXhpcyBvZiB3aGVyZSB0aGUgcHJlc3MgaGFwcGVuZWQuXG4gICAqIEBwYXJhbSAge251bWJlcn0gICB5IFRoZSBZLWF4aXMgb2Ygd2hlcmUgdGhlIHByZXNzIGhhcHBlbmVkLlxuICAgKiBAcGFyYW0gIHtudW1iZXJ9ICAgc2NhbGVFeHRyYSBFeHRyYSB6b29tLWluIHRvIGFwcGx5LlxuICAgKiBAcGFyYW0gIHtGdW5jdGlvbn0gW2NiPXRoaXMub3B0aW9ucy5vbkdyYWJdIEEgY2FsbGJhY2sgZnVuY3Rpb24gdGhhdFxuICAgKiB3aWxsIGJlIGNhbGxlZCB3aGVuIGEgdGFyZ2V0IGlzIGdyYWJiZWQgYW5kIHRyYW5zaXRpb24gaGFzIGVuZGVkLiBJdFxuICAgKiB3aWxsIGdldCB0aGUgdGFyZ2V0IGVsZW1lbnQgYXMgdGhlIGFyZ3VtZW50LlxuICAgKiBAcmV0dXJuIHt0aGlzfVxuICAgKi9cbiAgZ3JhYih4LCB5LCBzY2FsZUV4dHJhID0gdGhpcy5vcHRpb25zLnNjYWxlRXh0cmEsIGNiID0gdGhpcy5vcHRpb25zLm9uR3JhYikge1xuICAgIGlmICghdGhpcy5zaG93biB8fCB0aGlzLmxvY2spIHJldHVyblxuXG4gICAgY29uc3QgdGFyZ2V0ID0gdGhpcy50YXJnZXQuZWxcblxuICAgIHRoaXMub3B0aW9ucy5vbkJlZm9yZUdyYWIodGFyZ2V0KVxuXG4gICAgdGhpcy5yZWxlYXNlZCA9IGZhbHNlXG4gICAgdGhpcy50YXJnZXQuZ3JhYih4LCB5LCBzY2FsZUV4dHJhKVxuXG4gICAgY29uc3Qgb25HcmFiRW5kID0gKCkgPT4ge1xuICAgICAgbGlzdGVuKHRhcmdldCwgJ3RyYW5zaXRpb25lbmQnLCBvbkdyYWJFbmQsIGZhbHNlKVxuICAgICAgY2IodGFyZ2V0KVxuICAgIH1cblxuICAgIGxpc3Rlbih0YXJnZXQsICd0cmFuc2l0aW9uZW5kJywgb25HcmFiRW5kKVxuXG4gICAgcmV0dXJuIHRoaXNcbiAgfVxuXG4gIC8qKlxuICAgKiBNb3ZlIHRoZSBFbGVtZW50IGN1cnJlbnRseSBncmFiYmVkIGdpdmVuIGEgcG9zaXRpb24gYW5kIGFwcGx5IGV4dHJhIHpvb20taW4uXG4gICAqIEBwYXJhbSAge251bWJlcn0gICB4IFRoZSBYLWF4aXMgb2Ygd2hlcmUgdGhlIHByZXNzIGhhcHBlbmVkLlxuICAgKiBAcGFyYW0gIHtudW1iZXJ9ICAgeSBUaGUgWS1heGlzIG9mIHdoZXJlIHRoZSBwcmVzcyBoYXBwZW5lZC5cbiAgICogQHBhcmFtICB7bnVtYmVyfSAgIHNjYWxlRXh0cmEgRXh0cmEgem9vbS1pbiB0byBhcHBseS5cbiAgICogQHBhcmFtICB7RnVuY3Rpb259IFtjYj10aGlzLm9wdGlvbnMub25Nb3ZlXSBBIGNhbGxiYWNrIGZ1bmN0aW9uIHRoYXRcbiAgICogd2lsbCBiZSBjYWxsZWQgd2hlbiBhIHRhcmdldCBpcyBtb3ZlZCBhbmQgdHJhbnNpdGlvbiBoYXMgZW5kZWQuIEl0IHdpbGxcbiAgICogZ2V0IHRoZSB0YXJnZXQgZWxlbWVudCBhcyB0aGUgYXJndW1lbnQuXG4gICAqIEByZXR1cm4ge3RoaXN9XG4gICAqL1xuICBtb3ZlKHgsIHksIHNjYWxlRXh0cmEgPSB0aGlzLm9wdGlvbnMuc2NhbGVFeHRyYSwgY2IgPSB0aGlzLm9wdGlvbnMub25Nb3ZlKSB7XG4gICAgaWYgKCF0aGlzLnNob3duIHx8IHRoaXMubG9jaykgcmV0dXJuXG5cbiAgICB0aGlzLnJlbGVhc2VkID0gZmFsc2VcbiAgICB0aGlzLmJvZHkuc3R5bGUuY3Vyc29yID0gY3Vyc29yLm1vdmVcbiAgICB0aGlzLnRhcmdldC5tb3ZlKHgsIHksIHNjYWxlRXh0cmEpXG5cbiAgICBjb25zdCB0YXJnZXQgPSB0aGlzLnRhcmdldC5lbFxuXG4gICAgY29uc3Qgb25Nb3ZlRW5kID0gKCkgPT4ge1xuICAgICAgbGlzdGVuKHRhcmdldCwgJ3RyYW5zaXRpb25lbmQnLCBvbk1vdmVFbmQsIGZhbHNlKVxuICAgICAgY2IodGFyZ2V0KVxuICAgIH1cblxuICAgIGxpc3Rlbih0YXJnZXQsICd0cmFuc2l0aW9uZW5kJywgb25Nb3ZlRW5kKVxuXG4gICAgcmV0dXJuIHRoaXNcbiAgfVxuXG4gIC8qKlxuICAgKiBSZWxlYXNlIHRoZSBFbGVtZW50IGN1cnJlbnRseSBncmFiYmVkLlxuICAgKiBAcGFyYW0gIHtGdW5jdGlvbn0gW2NiPXRoaXMub3B0aW9ucy5vblJlbGVhc2VdIEEgY2FsbGJhY2sgZnVuY3Rpb24gdGhhdFxuICAgKiB3aWxsIGJlIGNhbGxlZCB3aGVuIGEgdGFyZ2V0IGlzIHJlbGVhc2VkIGFuZCB0cmFuc2l0aW9uIGhhcyBlbmRlZC4gSXRcbiAgICogd2lsbCBnZXQgdGhlIHRhcmdldCBlbGVtZW50IGFzIHRoZSBhcmd1bWVudC5cbiAgICogQHJldHVybiB7dGhpc31cbiAgICovXG4gIHJlbGVhc2UoY2IgPSB0aGlzLm9wdGlvbnMub25SZWxlYXNlKSB7XG4gICAgaWYgKCF0aGlzLnNob3duIHx8IHRoaXMubG9jaykgcmV0dXJuXG5cbiAgICBjb25zdCB0YXJnZXQgPSB0aGlzLnRhcmdldC5lbFxuXG4gICAgdGhpcy5vcHRpb25zLm9uQmVmb3JlUmVsZWFzZSh0YXJnZXQpXG5cbiAgICB0aGlzLmxvY2sgPSB0cnVlXG4gICAgdGhpcy5ib2R5LnN0eWxlLmN1cnNvciA9IGN1cnNvci5kZWZhdWx0XG4gICAgdGhpcy50YXJnZXQucmVzdG9yZU9wZW5TdHlsZSgpXG5cbiAgICBjb25zdCBvblJlbGVhc2VFbmQgPSAoKSA9PiB7XG4gICAgICBsaXN0ZW4odGFyZ2V0LCAndHJhbnNpdGlvbmVuZCcsIG9uUmVsZWFzZUVuZCwgZmFsc2UpXG4gICAgICB0aGlzLmxvY2sgPSBmYWxzZVxuICAgICAgdGhpcy5yZWxlYXNlZCA9IHRydWVcbiAgICAgIGNiKHRhcmdldClcbiAgICB9XG5cbiAgICBsaXN0ZW4odGFyZ2V0LCAndHJhbnNpdGlvbmVuZCcsIG9uUmVsZWFzZUVuZClcblxuICAgIHJldHVybiB0aGlzXG4gIH1cbn1cblxuZnVuY3Rpb24gdG9nZ2xlR3JhYkxpc3RlbmVycyhlbCwgaGFuZGxlciwgYWRkKSB7XG4gIGNvbnN0IHR5cGVzID0gW1xuICAgICdtb3VzZWRvd24nLFxuICAgICdtb3VzZW1vdmUnLFxuICAgICdtb3VzZXVwJyxcbiAgICAndG91Y2hzdGFydCcsXG4gICAgJ3RvdWNobW92ZScsXG4gICAgJ3RvdWNoZW5kJ1xuICBdXG5cbiAgdHlwZXMuZm9yRWFjaChmdW5jdGlvbiB0b2dnbGVMaXN0ZW5lcih0eXBlKSB7XG4gICAgbGlzdGVuKGVsLCB0eXBlLCBoYW5kbGVyW3R5cGVdLCBhZGQpXG4gIH0pXG59XG4iXSwibmFtZXMiOlsiY3Vyc29yIiwibGlzdGVuIiwiZWwiLCJldmVudCIsImhhbmRsZXIiLCJhZGQiLCJvcHRpb25zIiwicGFzc2l2ZSIsImFkZEV2ZW50TGlzdGVuZXIiLCJyZW1vdmVFdmVudExpc3RlbmVyIiwibG9hZEltYWdlIiwic3JjIiwiY2IiLCJpbWciLCJJbWFnZSIsIm9ubG9hZCIsIm9uSW1hZ2VMb2FkIiwiZ2V0T3JpZ2luYWxTb3VyY2UiLCJkYXRhc2V0Iiwib3JpZ2luYWwiLCJwYXJlbnROb2RlIiwidGFnTmFtZSIsImdldEF0dHJpYnV0ZSIsInNldFN0eWxlIiwic3R5bGVzIiwicmVtZW1iZXIiLCJ0cmFuc2l0aW9uIiwidmFsdWUiLCJ0cmFuc2Zvcm0iLCJzIiwic3R5bGUiLCJrZXkiLCJiaW5kQWxsIiwiX3RoaXMiLCJ0aGF0IiwibWV0aG9kcyIsIk9iamVjdCIsImdldE93blByb3BlcnR5TmFtZXMiLCJnZXRQcm90b3R5cGVPZiIsImZvckVhY2giLCJiaW5kT25lIiwibWV0aG9kIiwiYmluZCIsIm5vb3AiLCJQUkVTU19ERUxBWSIsImluc3RhbmNlIiwiZSIsInByZXZlbnREZWZhdWx0IiwiaXNQcmVzc2luZ01ldGFLZXkiLCJ3aW5kb3ciLCJvcGVuIiwidGFyZ2V0Iiwic3JjT3JpZ2luYWwiLCJjdXJyZW50VGFyZ2V0Iiwic2hvd24iLCJyZWxlYXNlZCIsImNsb3NlIiwicmVsZWFzZSIsImRvY3VtZW50IiwiZG9jdW1lbnRFbGVtZW50IiwiYm9keSIsInNjcm9sbExlZnQiLCJwYWdlWE9mZnNldCIsInNjcm9sbFRvcCIsInBhZ2VZT2Zmc2V0IiwibGFzdFNjcm9sbFBvc2l0aW9uIiwiZGVsdGFYIiwieCIsImRlbHRhWSIsInkiLCJ0aHJlc2hvbGQiLCJzY3JvbGxUaHJlc2hvbGQiLCJNYXRoIiwiYWJzIiwiaXNFc2NhcGUiLCJpc0xlZnRCdXR0b24iLCJjbGllbnRYIiwiY2xpZW50WSIsInByZXNzVGltZXIiLCJzZXRUaW1lb3V0IiwiZ3JhYk9uTW91c2VEb3duIiwiZ3JhYiIsIm1vdmUiLCJ0b3VjaGVzIiwiZ3JhYk9uVG91Y2hTdGFydCIsImlzVG91Y2hpbmciLCJidXR0b24iLCJtZXRhS2V5IiwiY3RybEtleSIsInRhcmdldFRvdWNoZXMiLCJsZW5ndGgiLCJjb2RlIiwia2V5Q29kZSIsImNyZWF0ZUVsZW1lbnQiLCJwYXJlbnQiLCJ1cGRhdGVTdHlsZSIsImNsaWNrT3ZlcmxheSIsInpJbmRleCIsImJnQ29sb3IiLCJ0cmFuc2l0aW9uRHVyYXRpb24iLCJ0cmFuc2l0aW9uVGltaW5nRnVuY3Rpb24iLCJhcHBlbmRDaGlsZCIsInJlbW92ZUNoaWxkIiwib2Zmc2V0V2lkdGgiLCJvcGFjaXR5IiwiYmdPcGFjaXR5IiwiVFJBTlNMQVRFX1oiLCJzcmNUaHVtYm5haWwiLCJzcmNzZXQiLCJyZWN0IiwiZ2V0Qm91bmRpbmdDbGllbnRSZWN0IiwidHJhbnNsYXRlIiwic2NhbGUiLCJzdHlsZU9wZW4iLCJzdHlsZUNsb3NlIiwiZW5hYmxlR3JhYiIsImNhbGN1bGF0ZVRyYW5zbGF0ZSIsImNhbGN1bGF0ZVNjYWxlIiwiem9vbU91dCIsImhlaWdodCIsIndpZHRoIiwic2NhbGVFeHRyYSIsIndpbmRvd0NlbnRlciIsImdldFdpbmRvd0NlbnRlciIsImR4IiwiZHkiLCJyZW1vdmVBdHRyaWJ1dGUiLCJ0ZW1wIiwiY2xvbmVOb2RlIiwic2V0QXR0cmlidXRlIiwicG9zaXRpb24iLCJ2aXNpYmlsaXR5IiwidXBkYXRlU3JjIiwidGFyZ2V0Q2VudGVyIiwibGVmdCIsInRvcCIsInpvb21pbmdIZWlnaHQiLCJ6b29taW5nV2lkdGgiLCJjdXN0b21TaXplIiwic2NhbGVCYXNlIiwidGFyZ2V0SGFsZldpZHRoIiwidGFyZ2V0SGFsZkhlaWdodCIsInRhcmdldEVkZ2VUb1dpbmRvd0VkZ2UiLCJzY2FsZUhvcml6b250YWxseSIsInNjYWxlVmVydGljYWxseSIsIm1pbiIsIm5hdHVyYWxXaWR0aCIsIm5hdHVyYWxIZWlnaHQiLCJtYXhab29taW5nV2lkdGgiLCJwYXJzZUZsb2F0IiwibWF4Wm9vbWluZ0hlaWdodCIsImRvY0VsIiwid2luZG93V2lkdGgiLCJjbGllbnRXaWR0aCIsImlubmVyV2lkdGgiLCJ3aW5kb3dIZWlnaHQiLCJjbGllbnRIZWlnaHQiLCJpbm5lckhlaWdodCIsIlpvb21pbmciLCJjcmVhdGUiLCJvdmVybGF5IiwibG9jayIsImJhYmVsSGVscGVycy5leHRlbmRzIiwiREVGQVVMVF9PUFRJT05TIiwiaW5pdCIsImVscyIsInF1ZXJ5U2VsZWN0b3JBbGwiLCJpIiwiem9vbUluIiwiY2xpY2siLCJwcmVsb2FkSW1hZ2UiLCJvbk9wZW4iLCJxdWVyeVNlbGVjdG9yIiwib25CZWZvcmVPcGVuIiwib25JbWFnZUxvYWRpbmciLCJvbkltYWdlTG9hZGVkIiwiaW5zZXJ0IiwiZmFkZUluIiwic2Nyb2xsIiwia2V5ZG93biIsImNsb3NlT25XaW5kb3dSZXNpemUiLCJyZXNpemVXaW5kb3ciLCJvbk9wZW5FbmQiLCJ1cGdyYWRlU291cmNlIiwib25DbG9zZSIsIm9uQmVmb3JlQ2xvc2UiLCJkZWZhdWx0IiwiZmFkZU91dCIsIm9uQ2xvc2VFbmQiLCJkb3duZ3JhZGVTb3VyY2UiLCJyZXN0b3JlQ2xvc2VTdHlsZSIsInJlbW92ZSIsIm9uR3JhYiIsIm9uQmVmb3JlR3JhYiIsIm9uR3JhYkVuZCIsIm9uTW92ZSIsIm9uTW92ZUVuZCIsIm9uUmVsZWFzZSIsIm9uQmVmb3JlUmVsZWFzZSIsInJlc3RvcmVPcGVuU3R5bGUiLCJvblJlbGVhc2VFbmQiLCJ0b2dnbGVHcmFiTGlzdGVuZXJzIiwidHlwZXMiLCJ0b2dnbGVMaXN0ZW5lciIsInR5cGUiXSwibWFwcGluZ3MiOiJBQUFPLElBQU1BLFNBQVM7V0FDWCxNQURXO1VBRVosU0FGWTtXQUdYLFVBSFc7UUFJZCxNQUpjO1FBS2Q7Q0FMRDs7QUFRUCxBQUFPLFNBQVNDLE1BQVQsQ0FBZ0JDLEVBQWhCLEVBQW9CQyxL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 952 | -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } -------------------------------------------------------------------------------- /cypress/integration/demo_spec.js: -------------------------------------------------------------------------------- 1 | describe('Demo Page Test', function () { 2 | before(function () { 3 | cy.visit('/index.html') 4 | }) 5 | 6 | it('Open, grab, release and close the default image', function () { 7 | cy.get('#img-default') 8 | .should(img => { 9 | expect(img).to.have.css('cursor').and.equal('zoom-in') 10 | }) 11 | .click() 12 | .wait(500) 13 | .should(img => { 14 | expect(img).to.have.css('cursor').and.equal('grab') 15 | expect(img).to.have.css('position').and.equal('relative') 16 | expect(img).to.have.css('z-index').and.equal('999') 17 | expect(img).to.have.css('transition') 18 | expect(img).to.have.css('transform') 19 | }) 20 | .trigger('mousedown', { button: 0 }) 21 | .wait(500) 22 | .should(img => { 23 | expect(img).to.have.css('cursor').and.equal('move') 24 | expect(img).to.have.css('position').and.equal('relative') 25 | expect(img).to.have.css('z-index').and.equal('999') 26 | expect(img).to.have.css('transition') 27 | expect(img).to.have.css('transform') 28 | }) 29 | .trigger('mouseup', { button: 0 }) 30 | .wait(500) 31 | .should(img => { 32 | expect(img).to.have.css('cursor').and.equal('grab') 33 | expect(img).to.have.css('position').and.equal('relative') 34 | expect(img).to.have.css('z-index').and.equal('999') 35 | expect(img).to.have.css('transition') 36 | expect(img).to.have.css('transform') 37 | }) 38 | .click() 39 | .wait(500) 40 | .should(img => { 41 | expect(img).to.have.css('cursor').and.equal('zoom-in') 42 | }) 43 | }) 44 | 45 | it('Open, grab, release and close the custom image', function () { 46 | cy.get('#img-custom') 47 | .should(img => { 48 | expect(img).to.have.css('cursor').and.equal('zoom-in') 49 | }) 50 | .click() 51 | .wait(500) 52 | .should(img => { 53 | expect(img).to.have.css('cursor').and.equal('grab') 54 | expect(img).to.have.css('position').and.equal('relative') 55 | expect(img).to.have.css('z-index').and.equal('999') 56 | expect(img).to.have.css('transition') 57 | expect(img).to.have.css('transform') 58 | }) 59 | .trigger('mousedown', { button: 0 }) 60 | .wait(500) 61 | .should(img => { 62 | expect(img).to.have.css('cursor').and.equal('move') 63 | expect(img).to.have.css('position').and.equal('relative') 64 | expect(img).to.have.css('z-index').and.equal('999') 65 | expect(img).to.have.css('transition') 66 | expect(img).to.have.css('transform') 67 | }) 68 | .trigger('mouseup', { button: 0 }) 69 | .wait(500) 70 | .should(img => { 71 | expect(img).to.have.css('cursor').and.equal('grab') 72 | expect(img).to.have.css('position').and.equal('relative') 73 | expect(img).to.have.css('z-index').and.equal('999') 74 | expect(img).to.have.css('transition') 75 | expect(img).to.have.css('transform') 76 | }) 77 | .click() 78 | .wait(500) 79 | .should(img => { 80 | expect(img).to.have.css('cursor').and.equal('zoom-in') 81 | }) 82 | }) 83 | }) 84 | 85 | -------------------------------------------------------------------------------- /cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example plugins/index.js can be used to load plugins 3 | // 4 | // You can change the location of this file or turn off loading 5 | // the plugins file with the 'pluginsFile' configuration option. 6 | // 7 | // You can read more here: 8 | // https://on.cypress.io/plugins-guide 9 | // *********************************************************** 10 | 11 | // This function is called when a project is opened or re-opened (e.g. due to 12 | // the project's config changing) 13 | 14 | module.exports = (on, config) => { 15 | // `on` is used to hook into various events Cypress emits 16 | // `config` is the resolved Cypress config 17 | } 18 | -------------------------------------------------------------------------------- /cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add("login", (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This is will overwrite an existing command -- 25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 26 | -------------------------------------------------------------------------------- /cypress/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingdido999/zooming/3d813c2f27bdebaeada3e34cd8ae3790b4b83d95/docs/.nojekyll -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | ## Quick start 4 | 5 | To experient with Zooming right away, try the following code on [codepen](https://codepen.io/kingdido999/pen/rpYrKV) or save it to local and open with a browser: 6 | 7 | ```html 8 | 9 | 10 | 11 | 12 | 29 | 30 | 31 | 32 |
33 |
34 |
35 |
36 |
37 |
38 | 39 | 40 | 41 | 42 | 50 | 51 | 52 | ``` 53 | 54 | ## Installation 55 | 56 | ``` 57 | yarn add zooming 58 | ``` 59 | 60 | Or: 61 | 62 | ``` 63 | npm install zooming --save 64 | ``` 65 | 66 | ## Usage 67 | 68 | The first thing is to import the library: 69 | 70 | ### Option 1: Script tag 71 | 72 | ```html 73 | 74 | ``` 75 | 76 | ### Option 2: Module loader 77 | 78 | ```javascript 79 | import Zooming from 'zooming' 80 | ``` 81 | 82 | Next, initialize Zooming instance after DOM content is fully loaded: 83 | 84 | ```js 85 | document.addEventListener('DOMContentLoaded', function () { 86 | const zooming = new Zooming({ 87 | // options... 88 | }) 89 | 90 | zooming.listen('.img-zoomable') 91 | }) 92 | ``` 93 | 94 | Now the target images are zoomable! 95 | 96 | !> Starting from Zooming **2.0**, we no longer listen to images with data attribute `data-action="zoom"` by default. Please make sure to call `.listen()` on target images after Zooming instance is created. 97 | 98 | ## What's Next? 99 | 100 | Check out [Guide](/guide) and [Configuration](/configuration). -------------------------------------------------------------------------------- /docs/api-reference.md: -------------------------------------------------------------------------------- 1 | # API Reference 2 | 3 | ## listen 4 | 5 | Make element(s) zoomable. The argument can either be a css selector or an Element. 6 | 7 | ```js 8 | const zooming = new Zooming() 9 | 10 | // By a css selector 11 | zooming.listen('.img-zoomable') 12 | 13 | // By an Element 14 | const img = document.getElementById('avatar') 15 | zooming.listen(img) 16 | ``` 17 | 18 | ## config 19 | 20 | Update options or return current options if no argument is provided. See [Configuration](/configuration) for more details. 21 | 22 | ```js 23 | const zooming = new Zooming() 24 | 25 | zooming.config({ 26 | // ... 27 | }) 28 | ``` 29 | 30 | ## open 31 | 32 | Open (zoom in) the Element. 33 | 34 | ```js 35 | const zooming = new Zooming() 36 | const img = document.getElementById('avatar') 37 | 38 | zooming.open(img, function onOpen(target) { 39 | // When the target is fully opened... 40 | }) 41 | ``` 42 | 43 | ## grab 44 | 45 | Grab the Element currently opened given a position and apply extra zoom-in. 46 | 47 | ```js 48 | const zooming = new Zooming() 49 | const [x, y] = [400, 600] 50 | const scaleExtra = 0.5 51 | 52 | zooming.grab(x, y, scaleExtra, function onGrab(target) { 53 | // When the target is fully grabbed... 54 | }) 55 | ``` 56 | 57 | ## move 58 | 59 | Move the Element currently grabbed given a position and apply extra zoom-in. 60 | 61 | ```js 62 | const zooming = new Zooming() 63 | const [x, y] = [400, 600] 64 | const scaleExtra = 0.5 65 | 66 | zooming.move(x, y, scaleExtra, function onMove(target) { 67 | // When the target is fully moved... 68 | }) 69 | ``` 70 | 71 | ## release 72 | 73 | Release the Element currently grabbed. 74 | 75 | ```js 76 | const zooming = new Zooming() 77 | 78 | zooming.release(function onRlease(target) { 79 | // When the target is fully released... 80 | }) 81 | ``` 82 | 83 | ## close 84 | 85 | Close (zoomout) the Element currently opened. 86 | 87 | ```js 88 | const zooming = new Zooming() 89 | 90 | zooming.close(function onClose(target) { 91 | // When the target is fully closed... 92 | }) 93 | ``` -------------------------------------------------------------------------------- /docs/configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | There are two types of configuration: [options](configuration?id=options) and [event hooks](configuration?id=event-hooks). 4 | 5 | Zooming instance takes an configuration object in the constructor function: 6 | 7 | ```js 8 | const zooming = new Zooming({ 9 | // ... 10 | }) 11 | ``` 12 | 13 | We also provide an API to change options: 14 | 15 | ```js 16 | const zooming = new Zooming() 17 | 18 | zooming.config({ 19 | // ... 20 | }) 21 | ``` 22 | 23 | ## Options 24 | 25 | An option modifies the appearence or the behavior of an instance. 26 | 27 | ### bgColor 28 | 29 | - Type: `String` 30 | - Default: `'rgb(255, 255, 255)'` 31 | 32 | Background color of overlay. 33 | 34 | ### bgOpacity 35 | 36 | - Type: `Number` 37 | - Default: `1` 38 | 39 | Background opacity of overlay. 40 | 41 | ### closeOnWindowResize 42 | 43 | - Type: `Boolean` 44 | - Default: `true` 45 | 46 | Close the zoomed image when browser window is resized. 47 | 48 | ### customSize 49 | 50 | - Type: `Object|String` 51 | - Default: `null` 52 | 53 | It defines the absolute image size after we zoom in the image. 54 | Note this option will ignore [scaleBase](configuration?id=scaleBase) if set. 55 | 56 | We could provide an object with `width` and `height` to specify size in pixel: 57 | 58 | ```js 59 | new Zooming({ 60 | customSize: { width: 800, height: 400 } 61 | }) 62 | ``` 63 | 64 | Alternatively, provide a percentage value relative to the original image size: 65 | 66 | ```js 67 | // Zoom at most to the original image size 68 | new Zooming({ 69 | customSize: '100%' 70 | }) 71 | 72 | // Zoom at most to the half of the orignal image size 73 | new Zooming({ 74 | customSize: '50%' 75 | }) 76 | ``` 77 | 78 | The **original image size** refers to `naturalWidth` and `naturalHeight` obtained from the image `src` attribute (not image source from `data-original`). However, we could override the **original image size** by specifying `data-zooming-width` and `data-zooming-height` on the image. 79 | 80 | ### enableGrab 81 | 82 | - Type: `Boolean` 83 | - Default: `true` 84 | 85 | Enable grabbing and dragging the image. Extra zoom-in will be applied. 86 | 87 | ### preloadImage 88 | 89 | - Type: `Boolean` 90 | - Default: `false` 91 | 92 | Preload zoomable images. Enabling this option might cause performance issue on a page with lots of images or large image size. 93 | 94 | ### scaleBase 95 | 96 | - Type: `Number` 97 | - Default: `1.0` 98 | 99 | The base scale factor for zooming. By default it scales to fit the browser window. 100 | 101 | ### scaleExtra 102 | 103 | - Type: `Number` 104 | - Default: `0.5` 105 | 106 | The additional scale factor while grabbing the image. 107 | 108 | ### scrollThreshold 109 | 110 | - Type: `Number` 111 | - Default: `40` 112 | 113 | How much scrolling it takes before closing out the instance. 114 | 115 | ### transitionDuration 116 | 117 | - Type: `Number` 118 | - Default: `0.4` 119 | 120 | Transition duration in seconds. 121 | 122 | ### transitionTimingFunction 123 | 124 | - Type: `String` 125 | - Default: `cubic-bezier(0.4, 0, 0, 1)` 126 | 127 | Transition timing function. 128 | 129 | ### zIndex 130 | 131 | - Type: `Number` 132 | - Default: `998` 133 | 134 | The z-index that the overlay will be added with. 135 | 136 | ## Event Hooks 137 | 138 | An event hook takes a callback function, which will be called upon a specific event occurs. 139 | 140 | Every callback function will get the target element as the argument, for example: 141 | 142 | ```js 143 | new Zooming({ 144 | onOpen: function (target) { 145 | // Now you have the target! 146 | } 147 | }) 148 | ``` 149 | 150 | Events are associated with following actions: `open`, `grab`, `move`, `release` and `close`. 151 | 152 | ### onBeforeOpen 153 | 154 | - Type: `Function` 155 | - Default: `function () {}` 156 | 157 | Occurs before opening the target. 158 | 159 | ### onOpen 160 | 161 | - Type: `Function` 162 | - Default: `function () {}` 163 | 164 | Occurs after the target is fully opened. 165 | 166 | ### onBeforeGrab 167 | 168 | - Type: `Function` 169 | - Default: `function () {}` 170 | 171 | Occurs before grabbing the target. 172 | 173 | ### onGrab 174 | 175 | - Type: `Function` 176 | - Default: `function () {}` 177 | 178 | Occurs after the target is fully grabbed. 179 | 180 | ### onMove 181 | 182 | - Type: `Function` 183 | - Default: `function () {}` 184 | 185 | Occurs after the target is fully moved. 186 | 187 | ### onBeforeRelease 188 | 189 | - Type: `Function` 190 | - Default: `function () {}` 191 | 192 | Occurs before releasing the target. 193 | 194 | ### onRelease 195 | 196 | - Type: `Function` 197 | - Default: `function () {}` 198 | 199 | Occurs after the target is fully released. 200 | 201 | ### onBeforeClose 202 | 203 | - Type: `Function` 204 | - Default: `function () {}` 205 | 206 | Occurs before closing the target. 207 | 208 | ### onClose 209 | 210 | - Type: `Function` 211 | - Default: `function () {}` 212 | 213 | Occurs after the target is fully closed. 214 | 215 | ### onImageLoading 216 | 217 | - Type: `Function` 218 | - Default: `function () {}` 219 | 220 | Occurs when the target hi-res image starts loading. This step happens right after `onBeforeOpen`. 221 | 222 | !> Triggers only if hi-res image source is supplied and option `preloadImage` is set to `false`. 223 | 224 | ### onImageLoaded 225 | 226 | - Type: `Function` 227 | - Default: `function () {}` 228 | 229 | Occurs when the target hi-res image has been loaded. 230 | 231 | !> Triggers only if hi-res image source is supplied and option `preloadImage` is set to `false`. -------------------------------------------------------------------------------- /docs/guide.md: -------------------------------------------------------------------------------- 1 | # Guide 2 | 3 | ## Zoom into a high resolution image 4 | 5 | ### Option 1: data attribute 6 | 7 | Add `data-original` attribute to the image: 8 | 9 | ```html 10 | 11 | ``` 12 | 13 | ### Option 2: anchor link 14 | 15 | Provide an original image link that wraps around the image: 16 | 17 | ```html 18 | 19 | 20 | 21 | ``` 22 | 23 | !> Notice that if both are provided, it takes the `data-original` value as hi-res image source. 24 | 25 | For the best result, the hi-res image should have the exact same aspect ratio as your regular image. 26 | 27 | ## Multiple instances 28 | 29 | To create multiple Zooming instances, each with its own [configuration](/configuration): 30 | 31 | ```js 32 | const zoomingLight = new Zooming({ 33 | bgColor: '#fff' 34 | }) 35 | 36 | const zoomingDark = new Zooming({ 37 | bgColor: '#000' 38 | }) 39 | ``` 40 | 41 | ## Specify image width and height 42 | 43 | To customize size for all images after zoom-in: see [customSize](/configuration?id=customSize) option. 44 | 45 | To set size for a specific image, we can leverage data attributes `data-zooming-width` and `data-zooming-height`. For example: 46 | 47 | ```html 48 | 53 | ``` 54 | 55 | ## Open the image in a new tab 56 | 57 | Click while holding your meta key (`⌘` or `Ctrl`). 58 | 59 | ## Working with React 60 | 61 | Please see this [example](https://github.com/kingdido999/atogatari/blob/master/client/src/components/ZoomableImage.js). 62 | 63 | Notice that it's best to pass in an initialized `zooming` instance as a prop to your component and use that instance to listen to your images within `componentDidMount` method. In this way, we don't create new `zooming` instances everytime the component rerendered. 64 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Zooming - Image zoom that makes sense. 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /docs/sidebar.md: -------------------------------------------------------------------------------- 1 | - [Getting Started](/) 2 | - [Guide](guide.md) 3 | - [Configuration](configuration.md) 4 | - [API Reference](api-reference.md) 5 | - [FAQ](https://github.com/kingdido999/zooming/issues?utf8=%E2%9C%93&q=label%3Aquestion%20) 6 | - [Change Logs](https://github.com/kingdido999/zooming/releases) -------------------------------------------------------------------------------- /examples/grid/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Grid 9 | 10 | 27 | 28 | 29 | 30 |
31 |
32 | 33 |
34 |
35 | 36 |
37 |
38 | 39 |
40 |
41 | 42 |
43 |
44 | 45 |
46 |
47 | 48 |
49 |
50 | 51 |
52 |
53 | 54 |
55 |
56 | 57 |
58 |
59 | 60 |
61 |
62 | 63 |
64 |
65 | 66 |
67 |
68 | 69 | 70 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /examples/image-size/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Image size 9 | 10 | 27 | 28 | 29 | 30 |
31 |
32 | 33 |
34 |
35 | 36 |
37 |
38 | 39 |
40 |
41 | 42 |
43 |
44 | 45 | 46 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | Zooming 10 | 11 | 12 | 13 | 14 | 16 | 17 | 18 | 20 | 21 | 22 | 24 | 25 | 26 | 27 | 71 | 72 | 73 | 74 | 75 | 76 | 78 |
79 | 80 |
81 |

Zooming

82 | 83 |
84 | journey_start_thumbnail 86 |
87 |
88 | 89 |
90 |

Image zoom that makes sense.

91 | 92 |
    93 |
  • Pure JavaScript & built with mobile in mind.
  • 94 |
  • Smooth animations with intuitive gestures.
  • 95 |
  • Zoom into a hi-res image if supplied.
  • 96 |
  • Easy to integrate & customizable.
  • 97 |
98 | 99 |
100 | 101 | journey_thumbnail 102 | 103 |
104 | 105 |

106 | Options below were designed to affect the second image only. 107 |

108 | 109 |
110 | faster 111 | dark 112 | smaller 113 |
114 | 115 |
116 | 117 |

118 | Faced with rolling sand dunes, age-old ruins, caves and howling winds, your passage will not be an easy one. The 119 | goal is to get to the mountaintop, but the experience is discovering who you are, what this place is, and what 120 | is your purpose. 121 |

122 | 123 |
124 | 125 | GitHub 126 | Get Started 127 |
128 | 129 |
130 |
131 | 132 |
133 | 134 | 140 | 141 | 143 | 144 | 216 | 218 | 219 | 220 | 221 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zooming", 3 | "version": "2.1.1", 4 | "description": "Image zoom that makes sense.", 5 | "main": "build/zooming.js", 6 | "module": "build/zooming.module.js", 7 | "jsnext:main": "build/zooming.module.js", 8 | "repository": { 9 | "type": "git", 10 | "url": "git@github.com:kingdido999/zooming.git" 11 | }, 12 | "keywords": [ 13 | "zoom", 14 | "image", 15 | "grab", 16 | "javascript" 17 | ], 18 | "scripts": { 19 | "start": "rollup -c -w -m inline", 20 | "build": "rollup -c && rollup -c rollup.config.min.js", 21 | "test": "cypress run --browser chrome", 22 | "test:open": "cypress open", 23 | "docs": "docsify serve ./docs" 24 | }, 25 | "author": "Desmond Ding ", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/kingdido999/zooming/issues" 29 | }, 30 | "homepage": "https://kingdido999.github.io/zooming", 31 | "devDependencies": { 32 | "babel-core": "6.26.3", 33 | "babel-plugin-transform-object-assign": "6.22.0", 34 | "babel-preset-es2015-rollup": "3.0.0", 35 | "chai": "4.1.2", 36 | "cypress": "4.3.0", 37 | "docsify-cli": "4.4.0", 38 | "husky": "4.2.3", 39 | "lint-staged": "10.2.2", 40 | "mocha": "7.1.1", 41 | "prettier-standard": "16.2.1", 42 | "rollup": "1.32.1", 43 | "rollup-plugin-babel": "3.0.3", 44 | "rollup-plugin-eslint": "4.0.0", 45 | "rollup-plugin-filesize": "6.2.1", 46 | "rollup-plugin-uglify": "6.0.4" 47 | }, 48 | "lint-staged": { 49 | "linters": { 50 | "src/**/*.js": [ 51 | "prettier-standard", 52 | "git add" 53 | ] 54 | } 55 | }, 56 | "husky": { 57 | "hooks": { 58 | "pre-commit": "lint-staged" 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel' 2 | import eslint from 'rollup-plugin-eslint' 3 | import filesize from 'rollup-plugin-filesize' 4 | import { main, module } from './package.json' 5 | 6 | const config = { 7 | input: 'src/index.js', 8 | plugins: [ 9 | babel(), 10 | eslint({ 11 | env: { 12 | browser: true 13 | }, 14 | extends: 'eslint:recommended', 15 | parserOptions: { 16 | ecmaVersion: 6, 17 | sourceType: 'module' 18 | }, 19 | rules: { 20 | indent: ['off', 2], 21 | quotes: ['error', 'single'], 22 | semi: ['off', 'never'] 23 | } 24 | }), 25 | filesize() 26 | ], 27 | output: [ 28 | { 29 | file: main, 30 | format: 'umd', 31 | name: 'Zooming' 32 | }, { 33 | file: module, 34 | format: 'es' 35 | } 36 | ] 37 | } 38 | 39 | export default config 40 | -------------------------------------------------------------------------------- /rollup.config.min.js: -------------------------------------------------------------------------------- 1 | import { uglify } from 'rollup-plugin-uglify' 2 | import defaultConfig from './rollup.config' 3 | 4 | const { plugins } = defaultConfig 5 | 6 | const minConfig = Object.assign({}, defaultConfig, { 7 | plugins: [...plugins, uglify()], 8 | output: { 9 | file: 'build/zooming.min.js', 10 | format: 'umd', 11 | name: 'Zooming' 12 | } 13 | }) 14 | 15 | export default minConfig 16 | -------------------------------------------------------------------------------- /src/handler.js: -------------------------------------------------------------------------------- 1 | import { bindAll } from './utils' 2 | 3 | const PRESS_DELAY = 200 4 | 5 | export default { 6 | init(instance) { 7 | bindAll(this, instance) 8 | }, 9 | 10 | click(e) { 11 | e.preventDefault() 12 | 13 | if (isPressingMetaKey(e)) { 14 | return window.open( 15 | this.target.srcOriginal || e.currentTarget.src, 16 | '_blank' 17 | ) 18 | } else { 19 | if (this.shown) { 20 | if (this.released) { 21 | this.close() 22 | } else { 23 | this.release() 24 | } 25 | } else { 26 | this.open(e.currentTarget) 27 | } 28 | } 29 | }, 30 | 31 | scroll() { 32 | const el = 33 | document.documentElement || document.body.parentNode || document.body 34 | const scrollLeft = window.pageXOffset || el.scrollLeft 35 | const scrollTop = window.pageYOffset || el.scrollTop 36 | 37 | if (this.lastScrollPosition === null) { 38 | this.lastScrollPosition = { 39 | x: scrollLeft, 40 | y: scrollTop 41 | } 42 | } 43 | 44 | const deltaX = this.lastScrollPosition.x - scrollLeft 45 | const deltaY = this.lastScrollPosition.y - scrollTop 46 | const threshold = this.options.scrollThreshold 47 | 48 | if (Math.abs(deltaY) >= threshold || Math.abs(deltaX) >= threshold) { 49 | this.lastScrollPosition = null 50 | this.close() 51 | } 52 | }, 53 | 54 | keydown(e) { 55 | if (isEscape(e)) { 56 | if (this.released) { 57 | this.close() 58 | } else { 59 | this.release(this.close) 60 | } 61 | } 62 | }, 63 | 64 | mousedown(e) { 65 | if (!isLeftButton(e) || isPressingMetaKey(e)) return 66 | e.preventDefault() 67 | const { clientX, clientY } = e 68 | 69 | this.pressTimer = setTimeout( 70 | function grabOnMouseDown() { 71 | this.grab(clientX, clientY) 72 | }.bind(this), 73 | PRESS_DELAY 74 | ) 75 | }, 76 | 77 | mousemove(e) { 78 | if (this.released) return 79 | this.move(e.clientX, e.clientY) 80 | }, 81 | 82 | mouseup(e) { 83 | if (!isLeftButton(e) || isPressingMetaKey(e)) return 84 | clearTimeout(this.pressTimer) 85 | 86 | if (this.released) { 87 | this.close() 88 | } else { 89 | this.release() 90 | } 91 | }, 92 | 93 | touchstart(e) { 94 | e.preventDefault() 95 | const { clientX, clientY } = e.touches[0] 96 | 97 | this.pressTimer = setTimeout( 98 | function grabOnTouchStart() { 99 | this.grab(clientX, clientY) 100 | }.bind(this), 101 | PRESS_DELAY 102 | ) 103 | }, 104 | 105 | touchmove(e) { 106 | if (this.released) return 107 | 108 | const { clientX, clientY } = e.touches[0] 109 | this.move(clientX, clientY) 110 | }, 111 | 112 | touchend(e) { 113 | if (isTouching(e)) return 114 | clearTimeout(this.pressTimer) 115 | 116 | if (this.released) { 117 | this.close() 118 | } else { 119 | this.release() 120 | } 121 | }, 122 | 123 | clickOverlay() { 124 | this.close() 125 | }, 126 | 127 | resizeWindow() { 128 | this.close() 129 | } 130 | } 131 | 132 | function isLeftButton(e) { 133 | return e.button === 0 134 | } 135 | 136 | function isPressingMetaKey(e) { 137 | return e.metaKey || e.ctrlKey 138 | } 139 | 140 | function isTouching(e) { 141 | e.targetTouches.length > 0 142 | } 143 | 144 | function isEscape(e) { 145 | const code = e.key || e.code 146 | return code === 'Escape' || e.keyCode === 27 147 | } 148 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { cursor, listen, loadImage, getOriginalSource } from './utils' 2 | import DEFAULT_OPTIONS from './options' 3 | 4 | import handler from './handler' 5 | import overlay from './overlay' 6 | import target from './target' 7 | 8 | /** 9 | * Zooming instance. 10 | */ 11 | export default class Zooming { 12 | /** 13 | * @param {Object} [options] Update default options if provided. 14 | */ 15 | constructor(options) { 16 | this.target = Object.create(target) 17 | this.overlay = Object.create(overlay) 18 | this.handler = Object.create(handler) 19 | this.body = document.body 20 | 21 | this.shown = false 22 | this.lock = false 23 | this.released = true 24 | this.lastScrollPosition = null 25 | this.pressTimer = null 26 | 27 | this.options = Object.assign({}, DEFAULT_OPTIONS, options) 28 | this.overlay.init(this) 29 | this.handler.init(this) 30 | } 31 | 32 | /** 33 | * Make element(s) zoomable. 34 | * @param {string|Element} el A css selector or an Element. 35 | * @return {this} 36 | */ 37 | listen(el) { 38 | if (typeof el === 'string') { 39 | const els = document.querySelectorAll(el) 40 | let i = els.length 41 | 42 | while (i--) { 43 | this.listen(els[i]) 44 | } 45 | } else if (el.tagName === 'IMG') { 46 | el.style.cursor = cursor.zoomIn 47 | listen(el, 'click', this.handler.click) 48 | 49 | if (this.options.preloadImage) { 50 | loadImage(getOriginalSource(el)) 51 | } 52 | } 53 | 54 | return this 55 | } 56 | 57 | /** 58 | * Update options or return current options if no argument is provided. 59 | * @param {Object} options An Object that contains this.options. 60 | * @return {this|this.options} 61 | */ 62 | config(options) { 63 | if (options) { 64 | Object.assign(this.options, options) 65 | this.overlay.updateStyle(this.options) 66 | return this 67 | } else { 68 | return this.options 69 | } 70 | } 71 | 72 | /** 73 | * Open (zoom in) the Element. 74 | * @param {Element} el The Element to open. 75 | * @param {Function} [cb=this.options.onOpen] A callback function that will 76 | * be called when a target is opened and transition has ended. It will get 77 | * the target element as the argument. 78 | * @return {this} 79 | */ 80 | open(el, cb = this.options.onOpen) { 81 | if (this.shown || this.lock) return 82 | 83 | const target = typeof el === 'string' ? document.querySelector(el) : el 84 | 85 | if (target.tagName !== 'IMG') return 86 | 87 | this.options.onBeforeOpen(target) 88 | 89 | this.target.init(target, this) 90 | 91 | if (!this.options.preloadImage) { 92 | const { srcOriginal } = this.target 93 | 94 | if (srcOriginal != null) { 95 | this.options.onImageLoading(target) 96 | loadImage(srcOriginal, this.options.onImageLoaded) 97 | } 98 | } 99 | 100 | this.shown = true 101 | this.lock = true 102 | 103 | this.target.zoomIn() 104 | this.overlay.insert() 105 | this.overlay.fadeIn() 106 | 107 | listen(document, 'scroll', this.handler.scroll) 108 | listen(document, 'keydown', this.handler.keydown) 109 | 110 | if (this.options.closeOnWindowResize) { 111 | listen(window, 'resize', this.handler.resizeWindow) 112 | } 113 | 114 | const onOpenEnd = () => { 115 | listen(target, 'transitionend', onOpenEnd, false) 116 | this.lock = false 117 | this.target.upgradeSource() 118 | 119 | if (this.options.enableGrab) { 120 | toggleGrabListeners(document, this.handler, true) 121 | } 122 | 123 | cb(target) 124 | } 125 | 126 | listen(target, 'transitionend', onOpenEnd) 127 | 128 | return this 129 | } 130 | 131 | /** 132 | * Close (zoom out) the Element currently opened. 133 | * @param {Function} [cb=this.options.onClose] A callback function that will 134 | * be called when a target is closed and transition has ended. It will get 135 | * the target element as the argument. 136 | * @return {this} 137 | */ 138 | close(cb = this.options.onClose) { 139 | if (!this.shown || this.lock) return 140 | 141 | const target = this.target.el 142 | 143 | this.options.onBeforeClose(target) 144 | 145 | this.lock = true 146 | this.body.style.cursor = cursor.default 147 | this.overlay.fadeOut() 148 | this.target.zoomOut() 149 | 150 | listen(document, 'scroll', this.handler.scroll, false) 151 | listen(document, 'keydown', this.handler.keydown, false) 152 | 153 | if (this.options.closeOnWindowResize) { 154 | listen(window, 'resize', this.handler.resizeWindow, false) 155 | } 156 | 157 | const onCloseEnd = () => { 158 | listen(target, 'transitionend', onCloseEnd, false) 159 | 160 | this.shown = false 161 | this.lock = false 162 | 163 | this.target.downgradeSource() 164 | 165 | if (this.options.enableGrab) { 166 | toggleGrabListeners(document, this.handler, false) 167 | } 168 | 169 | this.target.restoreCloseStyle() 170 | this.overlay.remove() 171 | 172 | cb(target) 173 | } 174 | 175 | listen(target, 'transitionend', onCloseEnd) 176 | 177 | return this 178 | } 179 | 180 | /** 181 | * Grab the Element currently opened given a position and apply extra zoom-in. 182 | * @param {number} x The X-axis of where the press happened. 183 | * @param {number} y The Y-axis of where the press happened. 184 | * @param {number} scaleExtra Extra zoom-in to apply. 185 | * @param {Function} [cb=this.options.onGrab] A callback function that 186 | * will be called when a target is grabbed and transition has ended. It 187 | * will get the target element as the argument. 188 | * @return {this} 189 | */ 190 | grab(x, y, scaleExtra = this.options.scaleExtra, cb = this.options.onGrab) { 191 | if (!this.shown || this.lock) return 192 | 193 | const target = this.target.el 194 | 195 | this.options.onBeforeGrab(target) 196 | 197 | this.released = false 198 | this.target.grab(x, y, scaleExtra) 199 | 200 | const onGrabEnd = () => { 201 | listen(target, 'transitionend', onGrabEnd, false) 202 | cb(target) 203 | } 204 | 205 | listen(target, 'transitionend', onGrabEnd) 206 | 207 | return this 208 | } 209 | 210 | /** 211 | * Move the Element currently grabbed given a position and apply extra zoom-in. 212 | * @param {number} x The X-axis of where the press happened. 213 | * @param {number} y The Y-axis of where the press happened. 214 | * @param {number} scaleExtra Extra zoom-in to apply. 215 | * @param {Function} [cb=this.options.onMove] A callback function that 216 | * will be called when a target is moved and transition has ended. It will 217 | * get the target element as the argument. 218 | * @return {this} 219 | */ 220 | move(x, y, scaleExtra = this.options.scaleExtra, cb = this.options.onMove) { 221 | if (!this.shown || this.lock) return 222 | 223 | this.released = false 224 | this.body.style.cursor = cursor.move 225 | this.target.move(x, y, scaleExtra) 226 | 227 | const target = this.target.el 228 | 229 | const onMoveEnd = () => { 230 | listen(target, 'transitionend', onMoveEnd, false) 231 | cb(target) 232 | } 233 | 234 | listen(target, 'transitionend', onMoveEnd) 235 | 236 | return this 237 | } 238 | 239 | /** 240 | * Release the Element currently grabbed. 241 | * @param {Function} [cb=this.options.onRelease] A callback function that 242 | * will be called when a target is released and transition has ended. It 243 | * will get the target element as the argument. 244 | * @return {this} 245 | */ 246 | release(cb = this.options.onRelease) { 247 | if (!this.shown || this.lock) return 248 | 249 | const target = this.target.el 250 | 251 | this.options.onBeforeRelease(target) 252 | 253 | this.lock = true 254 | this.body.style.cursor = cursor.default 255 | this.target.restoreOpenStyle() 256 | 257 | const onReleaseEnd = () => { 258 | listen(target, 'transitionend', onReleaseEnd, false) 259 | this.lock = false 260 | this.released = true 261 | cb(target) 262 | } 263 | 264 | listen(target, 'transitionend', onReleaseEnd) 265 | 266 | return this 267 | } 268 | } 269 | 270 | function toggleGrabListeners(el, handler, add) { 271 | const types = [ 272 | 'mousedown', 273 | 'mousemove', 274 | 'mouseup', 275 | 'touchstart', 276 | 'touchmove', 277 | 'touchend' 278 | ] 279 | 280 | types.forEach(function toggleListener(type) { 281 | listen(el, type, handler[type], add) 282 | }) 283 | } 284 | -------------------------------------------------------------------------------- /src/options.js: -------------------------------------------------------------------------------- 1 | const noop = () => {} 2 | 3 | export default { 4 | /** 5 | * To be able to grab and drag the image for extra zoom-in. 6 | * @type {boolean} 7 | */ 8 | enableGrab: true, 9 | 10 | /** 11 | * Preload zoomable images. 12 | * @type {boolean} 13 | */ 14 | preloadImage: false, 15 | 16 | /** 17 | * Close the zoomed image when browser window is resized. 18 | * @type {boolean} 19 | */ 20 | closeOnWindowResize: true, 21 | 22 | /** 23 | * Transition duration in seconds. 24 | * @type {number} 25 | */ 26 | transitionDuration: 0.4, 27 | 28 | /** 29 | * Transition timing function. 30 | * @type {string} 31 | */ 32 | transitionTimingFunction: 'cubic-bezier(0.4, 0, 0, 1)', 33 | 34 | /** 35 | * Overlay background color. 36 | * @type {string} 37 | */ 38 | bgColor: 'rgb(255, 255, 255)', 39 | 40 | /** 41 | * Overlay background opacity. 42 | * @type {number} 43 | */ 44 | bgOpacity: 1, 45 | 46 | /** 47 | * The base scale factor for zooming. By default scale to fit the window. 48 | * @type {number} 49 | */ 50 | scaleBase: 1.0, 51 | 52 | /** 53 | * The additional scale factor when grabbing the image. 54 | * @type {number} 55 | */ 56 | scaleExtra: 0.5, 57 | 58 | /** 59 | * How much scrolling it takes before closing out. 60 | * @type {number} 61 | */ 62 | scrollThreshold: 40, 63 | 64 | /** 65 | * The z-index that the overlay will be added with. 66 | * @type {number} 67 | */ 68 | zIndex: 998, 69 | 70 | /** 71 | * Scale (zoom in) to given width and height. Ignore scaleBase if set. 72 | * Alternatively, provide a percentage value relative to the original image size. 73 | * @type {Object|String} 74 | * @example 75 | * customSize: { width: 800, height: 400 } 76 | * customSize: 100% 77 | */ 78 | customSize: null, 79 | 80 | /** 81 | * A callback function that will be called when a target is opened and 82 | * transition has ended. It will get the target element as the argument. 83 | * @type {Function} 84 | */ 85 | onOpen: noop, 86 | 87 | /** 88 | * Same as above, except fired when closed. 89 | * @type {Function} 90 | */ 91 | onClose: noop, 92 | 93 | /** 94 | * Same as above, except fired when grabbed. 95 | * @type {Function} 96 | */ 97 | onGrab: noop, 98 | 99 | /** 100 | * Same as above, except fired when moved. 101 | * @type {Function} 102 | */ 103 | onMove: noop, 104 | 105 | /** 106 | * Same as above, except fired when released. 107 | * @type {Function} 108 | */ 109 | onRelease: noop, 110 | 111 | /** 112 | * A callback function that will be called before open. 113 | * @type {Function} 114 | */ 115 | onBeforeOpen: noop, 116 | 117 | /** 118 | * A callback function that will be called before close. 119 | * @type {Function} 120 | */ 121 | onBeforeClose: noop, 122 | 123 | /** 124 | * A callback function that will be called before grab. 125 | * @type {Function} 126 | */ 127 | onBeforeGrab: noop, 128 | 129 | /** 130 | * A callback function that will be called before release. 131 | * @type {Function} 132 | */ 133 | onBeforeRelease: noop, 134 | 135 | /** 136 | * A callback function that will be called when the hi-res image is loading. 137 | * @type {Function} 138 | */ 139 | onImageLoading: noop, 140 | 141 | /** 142 | * A callback function that will be called when the hi-res image is loaded. 143 | * @type {Function} 144 | */ 145 | onImageLoaded: noop 146 | } 147 | -------------------------------------------------------------------------------- /src/overlay.js: -------------------------------------------------------------------------------- 1 | import { listen, setStyle } from './utils' 2 | 3 | export default { 4 | init(instance) { 5 | this.el = document.createElement('div') 6 | this.instance = instance 7 | this.parent = document.body 8 | 9 | setStyle(this.el, { 10 | position: 'fixed', 11 | top: 0, 12 | left: 0, 13 | right: 0, 14 | bottom: 0, 15 | opacity: 0 16 | }) 17 | 18 | this.updateStyle(instance.options) 19 | listen(this.el, 'click', instance.handler.clickOverlay.bind(instance)) 20 | }, 21 | 22 | updateStyle(options) { 23 | setStyle(this.el, { 24 | zIndex: options.zIndex, 25 | backgroundColor: options.bgColor, 26 | transition: `opacity 27 | ${options.transitionDuration}s 28 | ${options.transitionTimingFunction}` 29 | }) 30 | }, 31 | 32 | insert() { 33 | this.parent.appendChild(this.el) 34 | }, 35 | 36 | remove() { 37 | this.parent.removeChild(this.el) 38 | }, 39 | 40 | fadeIn() { 41 | this.el.offsetWidth 42 | this.el.style.opacity = this.instance.options.bgOpacity 43 | }, 44 | 45 | fadeOut() { 46 | this.el.style.opacity = 0 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/target.js: -------------------------------------------------------------------------------- 1 | import { cursor, setStyle, getOriginalSource } from './utils' 2 | 3 | // Translate z-axis to fix CSS grid display issue in Chrome: 4 | // https://github.com/kingdido999/zooming/issues/42 5 | const TRANSLATE_Z = 0 6 | 7 | export default { 8 | init(el, instance) { 9 | this.el = el 10 | this.instance = instance 11 | this.srcThumbnail = this.el.getAttribute('src') 12 | this.srcset = this.el.getAttribute('srcset') 13 | this.srcOriginal = getOriginalSource(this.el) 14 | this.rect = this.el.getBoundingClientRect() 15 | this.translate = null 16 | this.scale = null 17 | this.styleOpen = null 18 | this.styleClose = null 19 | }, 20 | 21 | zoomIn() { 22 | const { 23 | zIndex, 24 | enableGrab, 25 | transitionDuration, 26 | transitionTimingFunction 27 | } = this.instance.options 28 | this.translate = this.calculateTranslate() 29 | this.scale = this.calculateScale() 30 | 31 | this.styleOpen = { 32 | position: 'relative', 33 | zIndex: zIndex + 1, 34 | cursor: enableGrab ? cursor.grab : cursor.zoomOut, 35 | transition: `transform 36 | ${transitionDuration}s 37 | ${transitionTimingFunction}`, 38 | transform: `translate3d(${this.translate.x}px, ${ 39 | this.translate.y 40 | }px, ${TRANSLATE_Z}px) 41 | scale(${this.scale.x},${this.scale.y})`, 42 | height: `${this.rect.height}px`, 43 | width: `${this.rect.width}px` 44 | } 45 | 46 | // Force layout update 47 | this.el.offsetWidth 48 | 49 | // Trigger transition 50 | this.styleClose = setStyle(this.el, this.styleOpen, true) 51 | }, 52 | 53 | zoomOut() { 54 | // Force layout update 55 | this.el.offsetWidth 56 | 57 | setStyle(this.el, { transform: 'none' }) 58 | }, 59 | 60 | grab(x, y, scaleExtra) { 61 | const windowCenter = getWindowCenter() 62 | const [dx, dy] = [windowCenter.x - x, windowCenter.y - y] 63 | 64 | setStyle(this.el, { 65 | cursor: cursor.move, 66 | transform: `translate3d( 67 | ${this.translate.x + dx}px, ${this.translate.y + 68 | dy}px, ${TRANSLATE_Z}px) 69 | scale(${this.scale.x + scaleExtra},${this.scale.y + scaleExtra})` 70 | }) 71 | }, 72 | 73 | move(x, y, scaleExtra) { 74 | const windowCenter = getWindowCenter() 75 | const [dx, dy] = [windowCenter.x - x, windowCenter.y - y] 76 | 77 | setStyle(this.el, { 78 | transition: 'transform', 79 | transform: `translate3d( 80 | ${this.translate.x + dx}px, ${this.translate.y + 81 | dy}px, ${TRANSLATE_Z}px) 82 | scale(${this.scale.x + scaleExtra},${this.scale.y + scaleExtra})` 83 | }) 84 | }, 85 | 86 | restoreCloseStyle() { 87 | setStyle(this.el, this.styleClose) 88 | }, 89 | 90 | restoreOpenStyle() { 91 | setStyle(this.el, this.styleOpen) 92 | }, 93 | 94 | upgradeSource() { 95 | if (this.srcOriginal) { 96 | const parentNode = this.el.parentNode 97 | 98 | if (this.srcset) { 99 | this.el.removeAttribute('srcset') 100 | } 101 | 102 | const temp = this.el.cloneNode(false) 103 | 104 | // Force compute the hi-res image in DOM to prevent 105 | // image flickering while updating src 106 | temp.setAttribute('src', this.srcOriginal) 107 | temp.style.position = 'fixed' 108 | temp.style.visibility = 'hidden' 109 | parentNode.appendChild(temp) 110 | 111 | // Add delay to prevent Firefox from flickering 112 | setTimeout( 113 | function updateSrc() { 114 | this.el.setAttribute('src', this.srcOriginal) 115 | parentNode.removeChild(temp) 116 | }.bind(this), 117 | 50 118 | ) 119 | } 120 | }, 121 | 122 | downgradeSource() { 123 | if (this.srcOriginal) { 124 | if (this.srcset) { 125 | this.el.setAttribute('srcset', this.srcset) 126 | } 127 | this.el.setAttribute('src', this.srcThumbnail) 128 | } 129 | }, 130 | 131 | calculateTranslate() { 132 | const windowCenter = getWindowCenter() 133 | const targetCenter = { 134 | x: this.rect.left + this.rect.width / 2, 135 | y: this.rect.top + this.rect.height / 2 136 | } 137 | 138 | // The vector to translate image to the window center 139 | return { 140 | x: windowCenter.x - targetCenter.x, 141 | y: windowCenter.y - targetCenter.y 142 | } 143 | }, 144 | 145 | calculateScale() { 146 | const { zoomingHeight, zoomingWidth } = this.el.dataset 147 | const { customSize, scaleBase } = this.instance.options 148 | 149 | if (!customSize && zoomingHeight && zoomingWidth) { 150 | return { 151 | x: zoomingWidth / this.rect.width, 152 | y: zoomingHeight / this.rect.height 153 | } 154 | } else if (customSize && typeof customSize === 'object') { 155 | return { 156 | x: customSize.width / this.rect.width, 157 | y: customSize.height / this.rect.height 158 | } 159 | } else { 160 | const targetHalfWidth = this.rect.width / 2 161 | const targetHalfHeight = this.rect.height / 2 162 | const windowCenter = getWindowCenter() 163 | 164 | // The distance between target edge and window edge 165 | const targetEdgeToWindowEdge = { 166 | x: windowCenter.x - targetHalfWidth, 167 | y: windowCenter.y - targetHalfHeight 168 | } 169 | 170 | const scaleHorizontally = targetEdgeToWindowEdge.x / targetHalfWidth 171 | const scaleVertically = targetEdgeToWindowEdge.y / targetHalfHeight 172 | 173 | // The additional scale is based on the smaller value of 174 | // scaling horizontally and scaling vertically 175 | const scale = scaleBase + Math.min(scaleHorizontally, scaleVertically) 176 | 177 | if (customSize && typeof customSize === 'string') { 178 | // Use zoomingWidth and zoomingHeight if available 179 | const naturalWidth = zoomingWidth || this.el.naturalWidth 180 | const naturalHeight = zoomingHeight || this.el.naturalHeight 181 | const maxZoomingWidth = 182 | parseFloat(customSize) * naturalWidth / (100 * this.rect.width) 183 | const maxZoomingHeight = 184 | parseFloat(customSize) * naturalHeight / (100 * this.rect.height) 185 | 186 | // Only scale image up to the specified customSize percentage 187 | if (scale > maxZoomingWidth || scale > maxZoomingHeight) { 188 | return { 189 | x: maxZoomingWidth, 190 | y: maxZoomingHeight 191 | } 192 | } 193 | } 194 | 195 | return { 196 | x: scale, 197 | y: scale 198 | } 199 | } 200 | } 201 | } 202 | 203 | function getWindowCenter() { 204 | const docEl = document.documentElement 205 | const windowWidth = Math.min(docEl.clientWidth, window.innerWidth) 206 | const windowHeight = Math.min(docEl.clientHeight, window.innerHeight) 207 | 208 | return { 209 | x: windowWidth / 2, 210 | y: windowHeight / 2 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | export const cursor = { 2 | default: 'auto', 3 | zoomIn: 'zoom-in', 4 | zoomOut: 'zoom-out', 5 | grab: 'grab', 6 | move: 'move' 7 | } 8 | 9 | export function listen(el, event, handler, add = true) { 10 | const options = { passive: false } 11 | 12 | if (add) { 13 | el.addEventListener(event, handler, options) 14 | } else { 15 | el.removeEventListener(event, handler, options) 16 | } 17 | } 18 | 19 | export function loadImage(src, cb) { 20 | if (src) { 21 | const img = new Image() 22 | 23 | img.onload = function onImageLoad() { 24 | if (cb) cb(img) 25 | } 26 | 27 | img.src = src 28 | } 29 | } 30 | 31 | export function getOriginalSource(el) { 32 | if (el.dataset.original) { 33 | return el.dataset.original 34 | } else if (el.parentNode.tagName === 'A') { 35 | return el.parentNode.getAttribute('href') 36 | } else { 37 | return null 38 | } 39 | } 40 | 41 | export function setStyle(el, styles, remember) { 42 | if (styles.transition) { 43 | const value = styles.transition 44 | delete styles.transition 45 | styles.transition = value 46 | } 47 | 48 | if (styles.transform) { 49 | const value = styles.transform 50 | delete styles.transform 51 | styles.transform = value 52 | } 53 | 54 | let s = el.style 55 | let original = {} 56 | 57 | for (let key in styles) { 58 | if (remember) { 59 | original[key] = s[key] || '' 60 | } 61 | 62 | s[key] = styles[key] 63 | } 64 | 65 | return original 66 | } 67 | 68 | export function bindAll(_this, that) { 69 | const methods = Object.getOwnPropertyNames(Object.getPrototypeOf(_this)) 70 | methods.forEach(function bindOne(method) { 71 | _this[method] = _this[method].bind(that) 72 | }) 73 | } 74 | --------------------------------------------------------------------------------