├── .gitignore ├── LICENSE ├── README.md ├── UPGRADE_GUIDE.md ├── babel.config.js ├── dist ├── wheel-zoom.js └── wheel-zoom.min.js ├── docker ├── Dockerfile └── start.sh ├── examples ├── html.html ├── image-rotate.html ├── image.html ├── images.html └── scale-reached.html ├── index.d.ts ├── package.json ├── prettier.config.js ├── rollup.config.js ├── src ├── calculator.js ├── default-options.js ├── observers │ ├── AbstractObserver.js │ ├── DragScrollableObserver.js │ ├── InteractionObserver.js │ └── PinchToZoomObserver.js ├── toolkit.js └── wheel-zoom.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 worka 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 | # vanilla-js-wheel-zoom 2 | 3 | Image resizing using mouse wheel (pinch to zoom) + drag scrollable image (as well as any HTML content) 4 | 5 | ![GitHub tag (latest by date)](https://img.shields.io/github/v/tag/worka/vanilla-js-wheel-zoom) 6 | [![GitHub stars](https://img.shields.io/github/stars/worka/vanilla-js-wheel-zoom)](https://github.com/worka/vanilla-js-wheel-zoom/stargazers) 7 | [![GitHub issues](https://img.shields.io/github/issues/worka/vanilla-js-wheel-zoom)](https://github.com/worka/vanilla-js-wheel-zoom/issues) 8 | [![GitHub forks](https://img.shields.io/github/forks/worka/vanilla-js-wheel-zoom)](https://github.com/worka/vanilla-js-wheel-zoom/network) 9 | [![](https://data.jsdelivr.com/v1/package/npm/vanilla-js-wheel-zoom/badge?style=rounded)](https://www.jsdelivr.com/package/npm/vanilla-js-wheel-zoom) 10 | 11 | Advantages: 12 | * the ability to fit the image into a container of any proportion 13 | * the ability to scale any HTML content 14 | * touch screen devices support 15 | 16 | > Starting with version 5, the plugin switched to using `style transform`. To use the plugin in older browsers, switch to earlier versions. 17 | 18 | > You need to center the image (or any HTML content) in the "viewport" in which scaling will take place. The "viewport" is taken automatically as the parent of the image in DOM. 19 | 20 | > HTML content can be of any structure, but the topmost child element in the “viewport” must be one. In the example with "badge" below, it will be more clear what is meant. 21 | 22 | 🖐 If you find my plugin helpful, please donate me 🤝 23 | 24 | Demo (only one image) 25 | 26 | Demo (multi images) 27 | 28 | Demo (html) 29 | 30 | ### Install 31 | 32 | ```cmd 33 | npm i vanilla-js-wheel-zoom 34 | ``` 35 | 36 | or 37 | 38 | ```cmd 39 | yarn add vanilla-js-wheel-zoom 40 | ``` 41 | 42 | ### Get started 43 | 44 | ```css 45 | #myViewport { 46 | display: flex; 47 | align-items: center; 48 | justify-content: center; 49 | } 50 | ``` 51 | 52 | ```html 53 |
54 | image 55 |
56 | ``` 57 | 58 | ``` javascript 59 | WZoom.create('#myContent'); 60 | ``` 61 | 62 | #### Syntax & Parameters 63 | 64 | ```javascript 65 | /** 66 | * Create WZoom instance 67 | * @param {string|HTMLElement} selectorOrHTMLElement 68 | * @param {Object} [options] 69 | * @returns {WZoom} 70 | */ 71 | const wzoom = WZoom.create(selectorOrHTMLElement[, options]); 72 | ``` 73 | 74 | #### Badge on the image 75 | 76 | ```css 77 | #myViewport { 78 | display: flex; 79 | align-items: center; 80 | justify-content: center; 81 | } 82 | 83 | #myBadge { 84 | position: absolute; 85 | border: solid 2px red; 86 | font-size: 80px; 87 | } 88 | 89 | #myImage { 90 | width: auto; 91 | height: auto; 92 | margin: auto; 93 | } 94 | ``` 95 | 96 | ``` html 97 |
98 |
99 |
Badge
100 | image 101 |
102 |
103 | ``` 104 | 105 | ``` javascript 106 | WZoom.create('#myContent', { 107 | type: 'html', 108 | width: 2500, 109 | height: 1500, 110 | }); 111 | ``` 112 | 113 | #### Control buttons 114 | 115 | ```html 116 | 117 | 118 | ``` 119 | 120 | ``` javascript 121 | const wzoom = WZoom.create('img'); 122 | 123 | document.querySelector('[data-zoom-up]').addEventListener('click', () => { 124 | wzoom.zoomUp(); 125 | }); 126 | 127 | document.querySelector('[data-zoom-down]').addEventListener('click', () => { 128 | wzoom.zoomDown(); 129 | }); 130 | ``` 131 | 132 | #### On window resize 133 | 134 | ``` javascript 135 | const wzoom = WZoom.create('img'); 136 | 137 | window.addEventListener('resize', () => { 138 | wzoom.prepare(); 139 | }); 140 | ``` 141 | 142 | #### How to rotate the image? 143 | 144 | [Try this 😉](https://github.com/worka/vanilla-js-wheel-zoom/issues/21) (and see demo) 145 | 146 | #### Callbacks onMaxScaleReached() / onMinScaleReached() 147 | 148 | There are no such, but [you can get](https://github.com/worka/vanilla-js-wheel-zoom/issues/34) the desired behavior (and see demo) 149 | 150 | #### Saving image state on page reload 151 | 152 | See demo 153 | 154 | #### Playground... 155 | 156 | Have some fun 🤸‍♂️ 157 | 158 | ### Options 159 | 160 | | name | type | default | note | 161 | |-----------------------|-------------|-------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 162 | | type | _String_ | `image` | `image` - if you need to scale only one image. In this case, there is no need to pass the parameters `width` and `height`. `html` - if you need to scale the HTML code. It is advisable to specify the parameters `width` and `height` that correspond to the original full size of the HTML content. | 163 | | width | _Number_ | `null` | For type `image` computed auto (if width set null), for type `html` need set real html content width, else computed auto. | 164 | | height | _Number_ | `null` | For type `image` computed auto (if height set null), for type `html` need set real html content height, else computed auto. | 165 | | minScale | _Number_ | `null` | The minimum scale to which the image can be zoomed.
If `falsy` or greater than `maxScale` then computed auto. | 166 | | maxScale | _Number_ | `1` | The maximum scale to which the image can be zoomed.
`1` means that the image can be maximized to 100%, `2` - 200%, etc. | 167 | | speed | _Number_ | `1.1` | Factor with which the image will be scaled.
The larger the value, the larger the step.
Can tend to `1`, but should not be equal to it (ex. `1.05`, `1.005`) or can be greater (ex. `1.5`, `2`, `5`, `10`) | 168 | | zoomOnClick | _Boolean_ | `true` | Zoom to maximum (minimum) size on click. | 169 | | zoomOnDblClick | _Boolean_ | `false` | Zoom to maximum (minimum) size on double click. If `true` then `zoomOnClick` = `false` | 170 | | prepare | _Function_ | `undefined` | Called after the script is initialized when the image is scaled and fit into the container. Gets `WZoom` instance as the first argument. | 171 | | rescale | _Function_ | `undefined` | Called on every change of scale. Gets `WZoom` instance as the first argument. | 172 | | alignContent | _String_ | `center` | Align content `center`, `left`, `top`, `right`, `bottom` | 173 | | smoothTime | _Number_ | `.25` | Time of smooth extinction. if `0` then no smooth extinction. Disabled for touch devices. (value in seconds) | 174 | | disableWheelZoom | _Boolean_ | `false` | | 175 | | reverseWheelDirection | _Boolean_ | `false` | Reverse wheel zoom direction | 176 | | | | | | 177 | | dragScrollable | _Boolean_ | `true` | If `true` - scaled image can be dragged with the mouse to see parts of the image that are out of scale. | 178 | | smoothTimeDrag | _Number_ | smoothTime | Optional override to `smoothTime` for mouse drag/pan actions.
Setting low (or 0) allows fluid drag actions, while maintaining zoom-smoothness from higher `smoothTime`.
If not provided, matches whatever `smoothTime` resolves to: `smoothTime`'s provided value or its default. | 179 | | onGrab | _Function_ | `undefined` | Called after grabbing an element. Gets the `event` and `WZoom` instance as the arguments. | 180 | | onMove | _Function_ | `undefined` | Called on every tick when moving element. Gets the `event` and `WZoom` instance as the arguments. | 181 | | onDrop | _Function_ | `undefined` | Called after dropping an element. Gets the `event` and `WZoom` instance as the arguments. | 182 | 183 | 184 | ### API 185 | 186 | | name | note | 187 | |------------------------------|----------------------------------------------------| 188 | | .prepare() | Reinitialize script | 189 | | .transform(top, left, scale) | Rebuild content state with passed params | 190 | | .zoomUp() | Zoom on one step (see option `speed`) | 191 | | .maxZoomUp() | Zoom to max scale | 192 | | .zoomDown() | Zoom out on one step (see option `speed`) | 193 | | .maxZoomDown() | Zoom to min scale | 194 | | .zoomUpToPoint({x, y}) | Zoom on one step to point (see option `speed`) | 195 | | .zoomDownToPoint({x, y}) | Zoom out on one step to point (see option `speed`) | 196 | | .maxZoomUpToPoint({x, y}) | Zoom to max scale to point | 197 | | .destroy() | Destroy object | 198 | 199 | ### License 200 | 201 | [MIT](https://choosealicense.com/licenses/mit/) 202 | -------------------------------------------------------------------------------- /UPGRADE_GUIDE.md: -------------------------------------------------------------------------------- 1 | ### How upgrade from 7.* to 8.* 2 | 3 | If you didn't use the `speed` option, then you don't need to do anything extra, otherwise you need to reconsider the value 4 | of this option. Read the description of the `speed` option. Now content scaling will be linear regardless of the scaling 5 | step. Thanks for the help [@n8w8](https://github.com/n8w8). Discussion here [#36](https://github.com/worka/vanilla-js-wheel-zoom/issues/36). 6 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ['@babel/env'] 4 | ] 5 | }; 6 | -------------------------------------------------------------------------------- /dist/wheel-zoom.js: -------------------------------------------------------------------------------- 1 | (function (global, factory) { 2 | typeof exports === 'object' && typeof module !== 'undefined' 3 | ? (module.exports = factory()) 4 | : typeof define === 'function' && define.amd 5 | ? define(factory) 6 | : ((global = 7 | typeof globalThis !== 'undefined' 8 | ? globalThis 9 | : global || self), 10 | (global.WZoom = factory())); 11 | })(this, function () { 12 | 'use strict'; 13 | 14 | function _arrayLikeToArray(r, a) { 15 | (null == a || a > r.length) && (a = r.length); 16 | for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; 17 | return n; 18 | } 19 | function _arrayWithHoles(r) { 20 | if (Array.isArray(r)) return r; 21 | } 22 | function _assertThisInitialized(e) { 23 | if (void 0 === e) 24 | throw new ReferenceError( 25 | "this hasn't been initialised - super() hasn't been called" 26 | ); 27 | return e; 28 | } 29 | function _callSuper(t, o, e) { 30 | return ( 31 | (o = _getPrototypeOf(o)), 32 | _possibleConstructorReturn( 33 | t, 34 | _isNativeReflectConstruct() 35 | ? Reflect.construct(o, [], _getPrototypeOf(t).constructor) 36 | : o.apply(t, e) 37 | ) 38 | ); 39 | } 40 | function _classCallCheck(a, n) { 41 | if (!(a instanceof n)) 42 | throw new TypeError('Cannot call a class as a function'); 43 | } 44 | function _defineProperties(e, r) { 45 | for (var t = 0; t < r.length; t++) { 46 | var o = r[t]; 47 | (o.enumerable = o.enumerable || !1), 48 | (o.configurable = !0), 49 | 'value' in o && (o.writable = !0), 50 | Object.defineProperty(e, _toPropertyKey(o.key), o); 51 | } 52 | } 53 | function _createClass(e, r, t) { 54 | return ( 55 | r && _defineProperties(e.prototype, r), 56 | Object.defineProperty(e, 'prototype', { 57 | writable: !1, 58 | }), 59 | e 60 | ); 61 | } 62 | function _createForOfIteratorHelper(r, e) { 63 | var t = 64 | ('undefined' != typeof Symbol && r[Symbol.iterator]) || 65 | r['@@iterator']; 66 | if (!t) { 67 | if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e) { 68 | t && (r = t); 69 | var n = 0, 70 | F = function () {}; 71 | return { 72 | s: F, 73 | n: function () { 74 | return n >= r.length 75 | ? { 76 | done: !0, 77 | } 78 | : { 79 | done: !1, 80 | value: r[n++], 81 | }; 82 | }, 83 | e: function (r) { 84 | throw r; 85 | }, 86 | f: F, 87 | }; 88 | } 89 | throw new TypeError( 90 | 'Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.' 91 | ); 92 | } 93 | var o, 94 | a = !0, 95 | u = !1; 96 | return { 97 | s: function () { 98 | t = t.call(r); 99 | }, 100 | n: function () { 101 | var r = t.next(); 102 | return (a = r.done), r; 103 | }, 104 | e: function (r) { 105 | (u = !0), (o = r); 106 | }, 107 | f: function () { 108 | try { 109 | a || null == t.return || t.return(); 110 | } finally { 111 | if (u) throw o; 112 | } 113 | }, 114 | }; 115 | } 116 | function _defineProperty(e, r, t) { 117 | return ( 118 | (r = _toPropertyKey(r)) in e 119 | ? Object.defineProperty(e, r, { 120 | value: t, 121 | enumerable: !0, 122 | configurable: !0, 123 | writable: !0, 124 | }) 125 | : (e[r] = t), 126 | e 127 | ); 128 | } 129 | function _get() { 130 | return ( 131 | (_get = 132 | 'undefined' != typeof Reflect && Reflect.get 133 | ? Reflect.get.bind() 134 | : function (e, t, r) { 135 | var p = _superPropBase(e, t); 136 | if (p) { 137 | var n = Object.getOwnPropertyDescriptor(p, t); 138 | return n.get 139 | ? n.get.call(arguments.length < 3 ? e : r) 140 | : n.value; 141 | } 142 | }), 143 | _get.apply(null, arguments) 144 | ); 145 | } 146 | function _getPrototypeOf(t) { 147 | return ( 148 | (_getPrototypeOf = Object.setPrototypeOf 149 | ? Object.getPrototypeOf.bind() 150 | : function (t) { 151 | return t.__proto__ || Object.getPrototypeOf(t); 152 | }), 153 | _getPrototypeOf(t) 154 | ); 155 | } 156 | function _inherits(t, e) { 157 | if ('function' != typeof e && null !== e) 158 | throw new TypeError( 159 | 'Super expression must either be null or a function' 160 | ); 161 | (t.prototype = Object.create(e && e.prototype, { 162 | constructor: { 163 | value: t, 164 | writable: !0, 165 | configurable: !0, 166 | }, 167 | })), 168 | Object.defineProperty(t, 'prototype', { 169 | writable: !1, 170 | }), 171 | e && _setPrototypeOf(t, e); 172 | } 173 | function _isNativeReflectConstruct() { 174 | try { 175 | var t = !Boolean.prototype.valueOf.call( 176 | Reflect.construct(Boolean, [], function () {}) 177 | ); 178 | } catch (t) {} 179 | return (_isNativeReflectConstruct = function () { 180 | return !!t; 181 | })(); 182 | } 183 | function _iterableToArrayLimit(r, l) { 184 | var t = 185 | null == r 186 | ? null 187 | : ('undefined' != typeof Symbol && r[Symbol.iterator]) || 188 | r['@@iterator']; 189 | if (null != t) { 190 | var e, 191 | n, 192 | i, 193 | u, 194 | a = [], 195 | f = !0, 196 | o = !1; 197 | try { 198 | if (((i = (t = t.call(r)).next), 0 === l)); 199 | else 200 | for ( 201 | ; 202 | !(f = (e = i.call(t)).done) && 203 | (a.push(e.value), a.length !== l); 204 | f = !0 205 | ); 206 | } catch (r) { 207 | (o = !0), (n = r); 208 | } finally { 209 | try { 210 | if ( 211 | !f && 212 | null != t.return && 213 | ((u = t.return()), Object(u) !== u) 214 | ) 215 | return; 216 | } finally { 217 | if (o) throw n; 218 | } 219 | } 220 | return a; 221 | } 222 | } 223 | function _nonIterableRest() { 224 | throw new TypeError( 225 | 'Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.' 226 | ); 227 | } 228 | function ownKeys(e, r) { 229 | var t = Object.keys(e); 230 | if (Object.getOwnPropertySymbols) { 231 | var o = Object.getOwnPropertySymbols(e); 232 | r && 233 | (o = o.filter(function (r) { 234 | return Object.getOwnPropertyDescriptor(e, r).enumerable; 235 | })), 236 | t.push.apply(t, o); 237 | } 238 | return t; 239 | } 240 | function _objectSpread2(e) { 241 | for (var r = 1; r < arguments.length; r++) { 242 | var t = null != arguments[r] ? arguments[r] : {}; 243 | r % 2 244 | ? ownKeys(Object(t), !0).forEach(function (r) { 245 | _defineProperty(e, r, t[r]); 246 | }) 247 | : Object.getOwnPropertyDescriptors 248 | ? Object.defineProperties( 249 | e, 250 | Object.getOwnPropertyDescriptors(t) 251 | ) 252 | : ownKeys(Object(t)).forEach(function (r) { 253 | Object.defineProperty( 254 | e, 255 | r, 256 | Object.getOwnPropertyDescriptor(t, r) 257 | ); 258 | }); 259 | } 260 | return e; 261 | } 262 | function _possibleConstructorReturn(t, e) { 263 | if (e && ('object' == typeof e || 'function' == typeof e)) return e; 264 | if (void 0 !== e) 265 | throw new TypeError( 266 | 'Derived constructors may only return object or undefined' 267 | ); 268 | return _assertThisInitialized(t); 269 | } 270 | function _setPrototypeOf(t, e) { 271 | return ( 272 | (_setPrototypeOf = Object.setPrototypeOf 273 | ? Object.setPrototypeOf.bind() 274 | : function (t, e) { 275 | return (t.__proto__ = e), t; 276 | }), 277 | _setPrototypeOf(t, e) 278 | ); 279 | } 280 | function _slicedToArray(r, e) { 281 | return ( 282 | _arrayWithHoles(r) || 283 | _iterableToArrayLimit(r, e) || 284 | _unsupportedIterableToArray(r, e) || 285 | _nonIterableRest() 286 | ); 287 | } 288 | function _superPropBase(t, o) { 289 | for ( 290 | ; 291 | !{}.hasOwnProperty.call(t, o) && null !== (t = _getPrototypeOf(t)); 292 | 293 | ); 294 | return t; 295 | } 296 | function _superPropGet(t, e, o, r) { 297 | var p = _get(_getPrototypeOf(t.prototype), e, o); 298 | return 'function' == typeof p 299 | ? function (t) { 300 | return p.apply(o, t); 301 | } 302 | : p; 303 | } 304 | function _toPrimitive(t, r) { 305 | if ('object' != typeof t || !t) return t; 306 | var e = t[Symbol.toPrimitive]; 307 | if (void 0 !== e) { 308 | var i = e.call(t, r); 309 | if ('object' != typeof i) return i; 310 | throw new TypeError('@@toPrimitive must return a primitive value.'); 311 | } 312 | return String(t); 313 | } 314 | function _toPropertyKey(t) { 315 | var i = _toPrimitive(t, 'string'); 316 | return 'symbol' == typeof i ? i : i + ''; 317 | } 318 | function _unsupportedIterableToArray(r, a) { 319 | if (r) { 320 | if ('string' == typeof r) return _arrayLikeToArray(r, a); 321 | var t = {}.toString.call(r).slice(8, -1); 322 | return ( 323 | 'Object' === t && r.constructor && (t = r.constructor.name), 324 | 'Map' === t || 'Set' === t 325 | ? Array.from(r) 326 | : 'Arguments' === t || 327 | /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) 328 | ? _arrayLikeToArray(r, a) 329 | : void 0 330 | ); 331 | } 332 | } 333 | 334 | /** 335 | * Get element position (with support old browsers) 336 | * @param {Element} element 337 | * @returns {{top: number, left: number}} 338 | */ 339 | function getElementPosition(element) { 340 | var box = element.getBoundingClientRect(); 341 | var _document = document, 342 | body = _document.body, 343 | documentElement = _document.documentElement; 344 | var scrollTop = getPageScrollTop(); 345 | var scrollLeft = getPageScrollLeft(); 346 | var clientTop = documentElement.clientTop || body.clientTop || 0; 347 | var clientLeft = documentElement.clientLeft || body.clientLeft || 0; 348 | var top = box.top + scrollTop - clientTop; 349 | var left = box.left + scrollLeft - clientLeft; 350 | return { 351 | top: top, 352 | left: left, 353 | }; 354 | } 355 | 356 | /** 357 | * Get page scroll left 358 | * @returns {number} 359 | */ 360 | function getPageScrollLeft() { 361 | var supportPageOffset = window.pageXOffset !== undefined; 362 | var isCSS1Compat = (document.compatMode || '') === 'CSS1Compat'; 363 | return supportPageOffset 364 | ? window.pageXOffset 365 | : isCSS1Compat 366 | ? document.documentElement.scrollLeft 367 | : document.body.scrollLeft; 368 | } 369 | 370 | /** 371 | * Get page scroll top 372 | * @returns {number} 373 | */ 374 | function getPageScrollTop() { 375 | var supportPageOffset = window.pageYOffset !== undefined; 376 | var isCSS1Compat = (document.compatMode || '') === 'CSS1Compat'; 377 | return supportPageOffset 378 | ? window.pageYOffset 379 | : isCSS1Compat 380 | ? document.documentElement.scrollTop 381 | : document.body.scrollTop; 382 | } 383 | 384 | /** 385 | * @param target 386 | * @param type 387 | * @param listener 388 | * @param options 389 | */ 390 | function on(target, type, listener) { 391 | var options = 392 | arguments.length > 3 && arguments[3] !== undefined 393 | ? arguments[3] 394 | : false; 395 | target.addEventListener(type, listener, options); 396 | } 397 | 398 | /** 399 | * @param target 400 | * @param type 401 | * @param listener 402 | * @param options 403 | */ 404 | function off(target, type, listener) { 405 | var options = 406 | arguments.length > 3 && arguments[3] !== undefined 407 | ? arguments[3] 408 | : false; 409 | target.removeEventListener(type, listener, options); 410 | } 411 | 412 | /** 413 | * @returns {boolean} 414 | */ 415 | function isTouch() { 416 | return ( 417 | 'ontouchstart' in window || 418 | navigator.MaxTouchPoints > 0 || 419 | navigator.msMaxTouchPoints > 0 420 | ); 421 | } 422 | 423 | /** 424 | * @param {Event} event 425 | * @returns {number} 426 | */ 427 | function eventClientX(event) { 428 | return event.type === 'wheel' || 429 | event.type === 'pointerup' || 430 | event.type === 'pointerdown' || 431 | event.type === 'pointermove' || 432 | event.type === 'mousedown' || 433 | event.type === 'mousemove' || 434 | event.type === 'mouseup' 435 | ? event.clientX 436 | : event.changedTouches[0].clientX; 437 | } 438 | 439 | /** 440 | * @param {Event} event 441 | * @returns {number} 442 | */ 443 | function eventClientY(event) { 444 | return event.type === 'wheel' || 445 | event.type === 'pointerup' || 446 | event.type === 'pointerdown' || 447 | event.type === 'pointermove' || 448 | event.type === 'mousedown' || 449 | event.type === 'mousemove' || 450 | event.type === 'mouseup' 451 | ? event.clientY 452 | : event.changedTouches[0].clientY; 453 | } 454 | 455 | /** 456 | * @param {HTMLElement} $element 457 | * @param {number} left 458 | * @param {number} top 459 | * @param {number} scale 460 | */ 461 | function transform($element, left, top, scale) { 462 | $element.style.transform = 'translate(' 463 | .concat(left, 'px, ') 464 | .concat(top, 'px) scale(') 465 | .concat(scale, ')'); 466 | } 467 | 468 | /** 469 | * @param {HTMLElement} $element 470 | * @param {number} time 471 | */ 472 | function transition($element, time) { 473 | if (time) { 474 | $element.style.transition = 'transform '.concat(time, 's'); 475 | } else { 476 | $element.style.removeProperty('transition'); 477 | } 478 | } 479 | 480 | /** 481 | * @param {WZoomViewport} viewport 482 | * @param {WZoomContent} content 483 | * @param {string} align 484 | * @returns {number[]} 485 | */ 486 | function calculateAlignPoint(viewport, content, align) { 487 | var pointX = 0; 488 | var pointY = 0; 489 | switch (align) { 490 | case 'top': 491 | pointY = (content.currentHeight - viewport.originalHeight) / 2; 492 | break; 493 | case 'right': 494 | pointX = 495 | ((content.currentWidth - viewport.originalWidth) / 2) * -1; 496 | break; 497 | case 'bottom': 498 | pointY = 499 | ((content.currentHeight - viewport.originalHeight) / 2) * 500 | -1; 501 | break; 502 | case 'left': 503 | pointX = (content.currentWidth - viewport.originalWidth) / 2; 504 | break; 505 | } 506 | return [pointX, pointY]; 507 | } 508 | 509 | /** 510 | * @param {WZoomViewport} viewport 511 | * @param {WZoomContent} content 512 | * @param {string} align 513 | * @returns {number[]} 514 | */ 515 | function calculateCorrectPoint(viewport, content, align) { 516 | var pointX = Math.max( 517 | 0, 518 | (viewport.originalWidth - content.currentWidth) / 2 519 | ); 520 | var pointY = Math.max( 521 | 0, 522 | (viewport.originalHeight - content.currentHeight) / 2 523 | ); 524 | switch (align) { 525 | case 'top': 526 | pointY = 0; 527 | break; 528 | case 'right': 529 | pointX = 0; 530 | break; 531 | case 'bottom': 532 | pointY = pointY * 2; 533 | break; 534 | case 'left': 535 | pointX = pointX * 2; 536 | break; 537 | } 538 | return [pointX, pointY]; 539 | } 540 | 541 | /** 542 | * @returns {number} 543 | */ 544 | function calculateContentShift( 545 | axisValue, 546 | axisScroll, 547 | axisViewportPosition, 548 | axisContentPosition, 549 | originalViewportSize, 550 | contentSizeRatio 551 | ) { 552 | var viewportShift = axisValue + axisScroll - axisViewportPosition; 553 | var centerViewportShift = originalViewportSize / 2 - viewportShift; 554 | var centerContentShift = centerViewportShift + axisContentPosition; 555 | return ( 556 | centerContentShift * contentSizeRatio - 557 | centerContentShift + 558 | axisContentPosition 559 | ); 560 | } 561 | function calculateContentMaxShift( 562 | align, 563 | originalViewportSize, 564 | correctCoordinate, 565 | size, 566 | shift 567 | ) { 568 | switch (align) { 569 | case 'left': 570 | if (size / 2 - shift < originalViewportSize / 2) { 571 | shift = (size - originalViewportSize) / 2; 572 | } 573 | break; 574 | case 'right': 575 | if (size / 2 + shift < originalViewportSize / 2) { 576 | shift = ((size - originalViewportSize) / 2) * -1; 577 | } 578 | break; 579 | default: 580 | if ( 581 | (size - originalViewportSize) / 2 + correctCoordinate < 582 | Math.abs(shift) 583 | ) { 584 | var positive = shift < 0 ? -1 : 1; 585 | shift = 586 | ((size - originalViewportSize) / 2 + 587 | correctCoordinate) * 588 | positive; 589 | } 590 | } 591 | return shift; 592 | } 593 | 594 | /** 595 | * @param {WZoomViewport} viewport 596 | * @returns {{x: number, y: number}} 597 | */ 598 | function calculateViewportCenter(viewport) { 599 | var viewportPosition = getElementPosition(viewport.$element); 600 | return { 601 | x: 602 | viewportPosition.left + 603 | viewport.originalWidth / 2 - 604 | getPageScrollLeft(), 605 | y: 606 | viewportPosition.top + 607 | viewport.originalHeight / 2 - 608 | getPageScrollTop(), 609 | }; 610 | } 611 | 612 | /** @type {WZoomOptions} */ 613 | var wZoomDefaultOptions = { 614 | // type content: `image` - only one image, `html` - any HTML content 615 | type: 'image', 616 | // for type `image` computed auto (if width set null), for type `html` need set real html content width, else computed auto 617 | width: null, 618 | // for type `image` computed auto (if height set null), for type `html` need set real html content height, else computed auto 619 | height: null, 620 | // minimum allowed proportion of scale (computed auto if null) 621 | minScale: null, 622 | // maximum allowed proportion of scale (1 = 100% content size) 623 | maxScale: 1, 624 | // content resizing speed 625 | speed: 1.1, 626 | // zoom to maximum (minimum) size on click 627 | zoomOnClick: true, 628 | // zoom to maximum (minimum) size on double click 629 | zoomOnDblClick: false, 630 | // smooth extinction 631 | smoothTime: 0.25, 632 | // align content `center`, `left`, `top`, `right`, `bottom` 633 | alignContent: 'center', 634 | // ******************** // 635 | disableWheelZoom: false, 636 | // option to reverse wheel direction 637 | reverseWheelDirection: false, 638 | // ******************** // 639 | // drag scrollable content 640 | dragScrollable: true, 641 | }; 642 | 643 | /** 644 | * @typedef WZoomOptions 645 | * @type {Object} 646 | * @property {string} type 647 | * @property {?number} width 648 | * @property {?number} height 649 | * @property {?number} minScale 650 | * @property {number} maxScale 651 | * @property {number} speed 652 | * @property {boolean} zoomOnClick 653 | * @property {boolean} zoomOnDblClick 654 | * @property {number} smoothTime 655 | * @property {string} alignContent 656 | * @property {boolean} disableWheelZoom 657 | * @property {boolean} reverseWheelDirection 658 | * @property {boolean} dragScrollable 659 | * @property {number} smoothTimeDrag 660 | * @property {?Function} onGrab 661 | * @property {?Function} onMove 662 | * @property {?Function} onDrop 663 | */ 664 | 665 | var AbstractObserver = /*#__PURE__*/ (function () { 666 | /** 667 | * @constructor 668 | */ 669 | function AbstractObserver() { 670 | _classCallCheck(this, AbstractObserver); 671 | /** @type {Object void>} */ 672 | this.subscribes = {}; 673 | } 674 | 675 | /** 676 | * @param {string} eventType 677 | * @param {(event: Event) => void} eventHandler 678 | * @returns {AbstractObserver} 679 | */ 680 | return _createClass(AbstractObserver, [ 681 | { 682 | key: 'on', 683 | value: function on(eventType, eventHandler) { 684 | if (!(eventType in this.subscribes)) { 685 | this.subscribes[eventType] = []; 686 | } 687 | this.subscribes[eventType].push(eventHandler); 688 | return this; 689 | }, 690 | }, 691 | { 692 | key: 'destroy', 693 | value: function destroy() { 694 | for (var key in this) { 695 | if (this.hasOwnProperty(key)) { 696 | this[key] = null; 697 | } 698 | } 699 | }, 700 | 701 | /** 702 | * @param {string} eventType 703 | * @param {Event} event 704 | * @protected 705 | */ 706 | }, 707 | { 708 | key: '_run', 709 | value: function _run(eventType, event) { 710 | if (this.subscribes[eventType]) { 711 | var _iterator = _createForOfIteratorHelper( 712 | this.subscribes[eventType] 713 | ), 714 | _step; 715 | try { 716 | for ( 717 | _iterator.s(); 718 | !(_step = _iterator.n()).done; 719 | 720 | ) { 721 | var eventHandler = _step.value; 722 | eventHandler(event); 723 | } 724 | } catch (err) { 725 | _iterator.e(err); 726 | } finally { 727 | _iterator.f(); 728 | } 729 | } 730 | }, 731 | }, 732 | ]); 733 | })(); 734 | 735 | var EVENT_GRAB = 'grab'; 736 | var EVENT_MOVE = 'move'; 737 | var EVENT_DROP = 'drop'; 738 | var DragScrollableObserver = /*#__PURE__*/ (function (_AbstractObserver) { 739 | /** 740 | * @param {HTMLElement} target 741 | * @constructor 742 | */ 743 | function DragScrollableObserver(target) { 744 | var _this; 745 | _classCallCheck(this, DragScrollableObserver); 746 | _this = _callSuper(this, DragScrollableObserver); 747 | _this.target = target; 748 | _this.moveTimer = null; 749 | _this.coordinates = null; 750 | _this.coordinatesShift = null; 751 | 752 | // check if we're using a touch screen 753 | _this.isTouch = isTouch(); 754 | // switch to touch events if using a touch screen 755 | _this.events = _this.isTouch 756 | ? { 757 | grab: 'touchstart', 758 | move: 'touchmove', 759 | drop: 'touchend', 760 | } 761 | : { 762 | grab: 'mousedown', 763 | move: 'mousemove', 764 | drop: 'mouseup', 765 | }; 766 | // for the touch screen we set the parameter forcibly 767 | _this.events.options = _this.isTouch 768 | ? { 769 | passive: false, 770 | } 771 | : false; 772 | _this._dropHandler = _this._dropHandler.bind(_this); 773 | _this._grabHandler = _this._grabHandler.bind(_this); 774 | _this._moveHandler = _this._moveHandler.bind(_this); 775 | on( 776 | _this.target, 777 | _this.events.grab, 778 | _this._grabHandler, 779 | _this.events.options 780 | ); 781 | return _this; 782 | } 783 | _inherits(DragScrollableObserver, _AbstractObserver); 784 | return _createClass(DragScrollableObserver, [ 785 | { 786 | key: 'destroy', 787 | value: function destroy() { 788 | off( 789 | this.target, 790 | this.events.grab, 791 | this._grabHandler, 792 | this.events.options 793 | ); 794 | off(document, this.events.drop, this._dropHandler); 795 | off(document, this.events.move, this._moveHandler); 796 | _superPropGet(DragScrollableObserver, 'destroy', this)([]); 797 | }, 798 | 799 | /** 800 | * @param {Event|TouchEvent|MouseEvent} event 801 | * @private 802 | */ 803 | }, 804 | { 805 | key: '_grabHandler', 806 | value: function _grabHandler(event) { 807 | // if touch started (only one finger) or pressed left mouse button 808 | if ( 809 | (this.isTouch && event.touches.length === 1) || 810 | event.buttons === 1 811 | ) { 812 | this.coordinates = { 813 | x: eventClientX(event), 814 | y: eventClientY(event), 815 | }; 816 | this.coordinatesShift = { 817 | x: 0, 818 | y: 0, 819 | }; 820 | on( 821 | document, 822 | this.events.drop, 823 | this._dropHandler, 824 | this.events.options 825 | ); 826 | on( 827 | document, 828 | this.events.move, 829 | this._moveHandler, 830 | this.events.options 831 | ); 832 | this._run(EVENT_GRAB, event); 833 | } 834 | }, 835 | 836 | /** 837 | * @param {Event} event 838 | * @private 839 | */ 840 | }, 841 | { 842 | key: '_dropHandler', 843 | value: function _dropHandler(event) { 844 | off(document, this.events.drop, this._dropHandler); 845 | off(document, this.events.move, this._moveHandler); 846 | this._run(EVENT_DROP, event); 847 | }, 848 | 849 | /** 850 | * @param {Event|TouchEvent} event 851 | * @private 852 | */ 853 | }, 854 | { 855 | key: '_moveHandler', 856 | value: function _moveHandler(event) { 857 | // so that it does not move when the touch screen and more than one finger 858 | if (this.isTouch && event.touches.length > 1) return false; 859 | var coordinatesShift = this.coordinatesShift, 860 | coordinates = this.coordinates; 861 | 862 | // change of the coordinate of the mouse cursor along the X/Y axis 863 | coordinatesShift.x = eventClientX(event) - coordinates.x; 864 | coordinatesShift.y = eventClientY(event) - coordinates.y; 865 | coordinates.x = eventClientX(event); 866 | coordinates.y = eventClientY(event); 867 | clearTimeout(this.moveTimer); 868 | 869 | // reset shift if cursor stops 870 | this.moveTimer = setTimeout(function () { 871 | coordinatesShift.x = 0; 872 | coordinatesShift.y = 0; 873 | }, 50); 874 | event.data = _objectSpread2( 875 | _objectSpread2({}, event.data || {}), 876 | {}, 877 | { 878 | x: coordinatesShift.x, 879 | y: coordinatesShift.y, 880 | } 881 | ); 882 | this._run(EVENT_MOVE, event); 883 | }, 884 | }, 885 | ]); 886 | })(AbstractObserver); 887 | 888 | var EVENT_CLICK = 'click'; 889 | var EVENT_DBLCLICK = 'dblclick'; 890 | var EVENT_WHEEL = 'wheel'; 891 | var InteractionObserver = /*#__PURE__*/ (function (_AbstractObserver) { 892 | /** 893 | * @param {HTMLElement} target 894 | * @constructor 895 | */ 896 | function InteractionObserver(target) { 897 | var _this; 898 | _classCallCheck(this, InteractionObserver); 899 | _this = _callSuper(this, InteractionObserver); 900 | _this.target = target; 901 | _this.coordsOnDown = null; 902 | _this.pressingTimeout = null; 903 | _this.firstClick = true; 904 | 905 | // check if we're using a touch screen 906 | _this.isTouch = isTouch(); 907 | // switch to touch events if using a touch screen 908 | _this.events = _this.isTouch 909 | ? { 910 | down: 'touchstart', 911 | up: 'touchend', 912 | } 913 | : { 914 | down: 'mousedown', 915 | up: 'mouseup', 916 | }; 917 | // if using touch screen tells the browser that the default action will not be undone 918 | _this.events.options = _this.isTouch 919 | ? { 920 | passive: true, 921 | } 922 | : false; 923 | _this._downHandler = _this._downHandler.bind(_this); 924 | _this._upHandler = _this._upHandler.bind(_this); 925 | _this._wheelHandler = _this._wheelHandler.bind(_this); 926 | on( 927 | _this.target, 928 | _this.events.down, 929 | _this._downHandler, 930 | _this.events.options 931 | ); 932 | on( 933 | _this.target, 934 | _this.events.up, 935 | _this._upHandler, 936 | _this.events.options 937 | ); 938 | on(_this.target, EVENT_WHEEL, _this._wheelHandler); 939 | return _this; 940 | } 941 | _inherits(InteractionObserver, _AbstractObserver); 942 | return _createClass(InteractionObserver, [ 943 | { 944 | key: 'destroy', 945 | value: function destroy() { 946 | off( 947 | this.target, 948 | this.events.down, 949 | this._downHandler, 950 | this.events.options 951 | ); 952 | off( 953 | this.target, 954 | this.events.up, 955 | this._upHandler, 956 | this.events.options 957 | ); 958 | off( 959 | this.target, 960 | EVENT_WHEEL, 961 | this._wheelHandler, 962 | this.events.options 963 | ); 964 | _superPropGet(InteractionObserver, 'destroy', this)([]); 965 | }, 966 | 967 | /** 968 | * @param {TouchEvent|MouseEvent|PointerEvent} event 969 | * @private 970 | */ 971 | }, 972 | { 973 | key: '_downHandler', 974 | value: function _downHandler(event) { 975 | this.coordsOnDown = null; 976 | if ( 977 | (this.isTouch && event.touches.length === 1) || 978 | event.buttons === 1 979 | ) { 980 | this.coordsOnDown = { 981 | x: eventClientX(event), 982 | y: eventClientY(event), 983 | }; 984 | } 985 | clearTimeout(this.pressingTimeout); 986 | }, 987 | 988 | /** 989 | * @param {TouchEvent|MouseEvent|PointerEvent} event 990 | * @private 991 | */ 992 | }, 993 | { 994 | key: '_upHandler', 995 | value: function _upHandler(event) { 996 | var _this2 = this; 997 | var delay = 200; 998 | var setTimeoutInner = this.subscribes[EVENT_DBLCLICK] 999 | ? setTimeout 1000 | : function (cb, delay) { 1001 | return cb(); 1002 | }; 1003 | if (this.firstClick) { 1004 | this.firstClick = false; 1005 | this.pressingTimeout = setTimeoutInner(function () { 1006 | if (!_this2._isDetectedShift(event)) { 1007 | _this2._run(EVENT_CLICK, event); 1008 | } 1009 | _this2.firstClick = true; 1010 | }, delay); 1011 | } else { 1012 | this.pressingTimeout = setTimeoutInner(function () { 1013 | if (!_this2._isDetectedShift(event)) { 1014 | _this2._run(EVENT_DBLCLICK, event); 1015 | } 1016 | _this2.firstClick = true; 1017 | }, delay / 2); 1018 | } 1019 | }, 1020 | 1021 | /** 1022 | * @param {WheelEvent} event 1023 | * @private 1024 | */ 1025 | }, 1026 | { 1027 | key: '_wheelHandler', 1028 | value: function _wheelHandler(event) { 1029 | this._run(EVENT_WHEEL, event); 1030 | }, 1031 | 1032 | /** 1033 | * @param {TouchEvent|MouseEvent|PointerEvent} event 1034 | * @return {boolean} 1035 | * @private 1036 | */ 1037 | }, 1038 | { 1039 | key: '_isDetectedShift', 1040 | value: function _isDetectedShift(event) { 1041 | return !( 1042 | this.coordsOnDown && 1043 | this.coordsOnDown.x === eventClientX(event) && 1044 | this.coordsOnDown.y === eventClientY(event) 1045 | ); 1046 | }, 1047 | }, 1048 | ]); 1049 | })(AbstractObserver); 1050 | 1051 | var EVENT_PINCH_TO_ZOOM = 'pinchtozoom'; 1052 | var SHIFT_DECIDE_THAT_MOVE_STARTED = 5; 1053 | var PinchToZoomObserver = /*#__PURE__*/ (function (_AbstractObserver) { 1054 | /** 1055 | * @param {HTMLElement} target 1056 | * @constructor 1057 | */ 1058 | function PinchToZoomObserver(target) { 1059 | var _this; 1060 | _classCallCheck(this, PinchToZoomObserver); 1061 | _this = _callSuper(this, PinchToZoomObserver); 1062 | _this.target = target; 1063 | _this.fingersHypot = null; 1064 | _this.zoomPinchWasDetected = false; 1065 | _this._touchMoveHandler = _this._touchMoveHandler.bind(_this); 1066 | _this._touchEndHandler = _this._touchEndHandler.bind(_this); 1067 | on(_this.target, 'touchmove', _this._touchMoveHandler); 1068 | on(_this.target, 'touchend', _this._touchEndHandler); 1069 | return _this; 1070 | } 1071 | _inherits(PinchToZoomObserver, _AbstractObserver); 1072 | return _createClass(PinchToZoomObserver, [ 1073 | { 1074 | key: 'destroy', 1075 | value: function destroy() { 1076 | off(this.target, 'touchmove', this._touchMoveHandler); 1077 | off(this.target, 'touchend', this._touchEndHandler); 1078 | _superPropGet(PinchToZoomObserver, 'destroy', this)([]); 1079 | }, 1080 | 1081 | /** 1082 | * @param {TouchEvent|PointerEvent} event 1083 | * @private 1084 | */ 1085 | }, 1086 | { 1087 | key: '_touchMoveHandler', 1088 | value: function _touchMoveHandler(event) { 1089 | // detect two fingers 1090 | if (event.targetTouches.length === 2) { 1091 | var pageX1 = event.targetTouches[0].clientX; 1092 | var pageY1 = event.targetTouches[0].clientY; 1093 | var pageX2 = event.targetTouches[1].clientX; 1094 | var pageY2 = event.targetTouches[1].clientY; 1095 | 1096 | // Math.hypot() analog 1097 | var fingersHypotNew = Math.round( 1098 | Math.sqrt( 1099 | Math.pow(Math.abs(pageX1 - pageX2), 2) + 1100 | Math.pow(Math.abs(pageY1 - pageY2), 2) 1101 | ) 1102 | ); 1103 | var direction = 0; 1104 | if ( 1105 | fingersHypotNew > 1106 | this.fingersHypot + SHIFT_DECIDE_THAT_MOVE_STARTED 1107 | ) 1108 | direction = -1; 1109 | if ( 1110 | fingersHypotNew < 1111 | this.fingersHypot - SHIFT_DECIDE_THAT_MOVE_STARTED 1112 | ) 1113 | direction = 1; 1114 | if (direction !== 0) { 1115 | if (this.fingersHypot !== null || direction === 1) { 1116 | // middle position between fingers 1117 | var clientX = 1118 | Math.min(pageX1, pageX2) + 1119 | Math.abs(pageX1 - pageX2) / 2; 1120 | var clientY = 1121 | Math.min(pageY1, pageY2) + 1122 | Math.abs(pageY1 - pageY2) / 2; 1123 | event.data = _objectSpread2( 1124 | _objectSpread2({}, event.data || {}), 1125 | {}, 1126 | { 1127 | clientX: clientX, 1128 | clientY: clientY, 1129 | direction: direction, 1130 | } 1131 | ); 1132 | this._run(EVENT_PINCH_TO_ZOOM, event); 1133 | } 1134 | this.fingersHypot = fingersHypotNew; 1135 | this.zoomPinchWasDetected = true; 1136 | } 1137 | } 1138 | }, 1139 | 1140 | /** 1141 | * @private 1142 | */ 1143 | }, 1144 | { 1145 | key: '_touchEndHandler', 1146 | value: function _touchEndHandler() { 1147 | if (this.zoomPinchWasDetected) { 1148 | this.fingersHypot = null; 1149 | this.zoomPinchWasDetected = false; 1150 | } 1151 | }, 1152 | }, 1153 | ]); 1154 | })(AbstractObserver); 1155 | 1156 | /** 1157 | * @class WZoom 1158 | * @param {string|HTMLElement} selectorOrHTMLElement 1159 | * @param {WZoomOptions} options 1160 | * @constructor 1161 | */ 1162 | function WZoom(selectorOrHTMLElement) { 1163 | var options = 1164 | arguments.length > 1 && arguments[1] !== undefined 1165 | ? arguments[1] 1166 | : {}; 1167 | this._init = this._init.bind(this); 1168 | this._prepare = this._prepare.bind(this); 1169 | this._computeScale = this._computeScale.bind(this); 1170 | this._computePosition = this._computePosition.bind(this); 1171 | this._transform = this._transform.bind(this); 1172 | 1173 | /** @type {WZoomContent} */ 1174 | this.content = {}; 1175 | if (typeof selectorOrHTMLElement === 'string') { 1176 | this.content.$element = document.querySelector( 1177 | selectorOrHTMLElement 1178 | ); 1179 | if (!this.content.$element) { 1180 | throw 'WZoom: Element with selector `'.concat( 1181 | selectorOrHTMLElement, 1182 | '` not found' 1183 | ); 1184 | } 1185 | } else if (selectorOrHTMLElement instanceof HTMLElement) { 1186 | this.content.$element = selectorOrHTMLElement; 1187 | } else { 1188 | throw 'WZoom: `selectorOrHTMLElement` must be selector or HTMLElement, and not '.concat( 1189 | {}.toString.call(selectorOrHTMLElement) 1190 | ); 1191 | } 1192 | 1193 | /** @type {WZoomViewport} */ 1194 | this.viewport = {}; 1195 | // for viewport take just the parent 1196 | this.viewport.$element = this.content.$element.parentElement; 1197 | 1198 | /** @type {WZoomOptions} */ 1199 | this.options = optionsConstructor(options, wZoomDefaultOptions); 1200 | 1201 | // check if we're using a touch screen 1202 | this.isTouch = isTouch(); 1203 | this.direction = 1; 1204 | /** @type {AbstractObserver[]} */ 1205 | this.observers = []; 1206 | if (this.options.type === 'image') { 1207 | // if the `image` has already been loaded 1208 | if (this.content.$element.complete) { 1209 | this._init(); 1210 | } else { 1211 | on(this.content.$element, 'load', this._init, { 1212 | once: true, 1213 | }); 1214 | } 1215 | } else { 1216 | this._init(); 1217 | } 1218 | } 1219 | WZoom.prototype = { 1220 | constructor: WZoom, 1221 | /** 1222 | * @private 1223 | */ 1224 | _init: function _init() { 1225 | var _this = this; 1226 | var viewport = this.viewport, 1227 | content = this.content, 1228 | options = this.options, 1229 | observers = this.observers; 1230 | this._prepare(); 1231 | this._destroyObservers(); 1232 | if (options.dragScrollable === true) { 1233 | var dragScrollableObserver = new DragScrollableObserver( 1234 | content.$element 1235 | ); 1236 | observers.push(dragScrollableObserver); 1237 | if (typeof options.onGrab === 'function') { 1238 | dragScrollableObserver.on(EVENT_GRAB, function (event) { 1239 | event.preventDefault(); 1240 | options.onGrab(event, _this); 1241 | }); 1242 | } 1243 | if (typeof options.onDrop === 'function') { 1244 | dragScrollableObserver.on(EVENT_DROP, function (event) { 1245 | event.preventDefault(); 1246 | options.onDrop(event, _this); 1247 | }); 1248 | } 1249 | dragScrollableObserver.on(EVENT_MOVE, function (event) { 1250 | event.preventDefault(); 1251 | var _event$data = event.data, 1252 | x = _event$data.x, 1253 | y = _event$data.y; 1254 | var contentNewLeft = content.currentLeft + x; 1255 | var contentNewTop = content.currentTop + y; 1256 | var maxAvailableLeft = 1257 | (content.currentWidth - viewport.originalWidth) / 2 + 1258 | content.correctX; 1259 | var maxAvailableTop = 1260 | (content.currentHeight - viewport.originalHeight) / 2 + 1261 | content.correctY; 1262 | 1263 | // if we do not go beyond the permissible boundaries of the viewport 1264 | if (Math.abs(contentNewLeft) <= maxAvailableLeft) 1265 | content.currentLeft = contentNewLeft; 1266 | // if we do not go beyond the permissible boundaries of the viewport 1267 | if (Math.abs(contentNewTop) <= maxAvailableTop) 1268 | content.currentTop = contentNewTop; 1269 | _this._transform(options.smoothTimeDrag); 1270 | if (typeof options.onMove === 'function') { 1271 | options.onMove(event, _this); 1272 | } 1273 | }); 1274 | } 1275 | var interactionObserver = new InteractionObserver(content.$element); 1276 | observers.push(interactionObserver); 1277 | if (!options.disableWheelZoom) { 1278 | if (this.isTouch) { 1279 | var pinchToZoomObserver = new PinchToZoomObserver( 1280 | content.$element 1281 | ); 1282 | observers.push(pinchToZoomObserver); 1283 | pinchToZoomObserver.on( 1284 | EVENT_PINCH_TO_ZOOM, 1285 | function (event) { 1286 | var _event$data2 = event.data, 1287 | clientX = _event$data2.clientX, 1288 | clientY = _event$data2.clientY, 1289 | direction = _event$data2.direction; 1290 | var scale = _this._computeScale(direction); 1291 | _this._computePosition(scale, clientX, clientY); 1292 | _this._transform(); 1293 | } 1294 | ); 1295 | } else { 1296 | interactionObserver.on(EVENT_WHEEL, function (event) { 1297 | event.preventDefault(); 1298 | var direction = options.reverseWheelDirection 1299 | ? -event.deltaY 1300 | : event.deltaY; 1301 | var scale = _this._computeScale(direction); 1302 | _this._computePosition( 1303 | scale, 1304 | eventClientX(event), 1305 | eventClientY(event) 1306 | ); 1307 | _this._transform(); 1308 | }); 1309 | } 1310 | } 1311 | if (options.zoomOnClick || options.zoomOnDblClick) { 1312 | var eventType = options.zoomOnDblClick 1313 | ? EVENT_DBLCLICK 1314 | : EVENT_CLICK; 1315 | interactionObserver.on(eventType, function (event) { 1316 | var scale = 1317 | _this.direction === 1 1318 | ? content.maxScale 1319 | : content.minScale; 1320 | _this._computePosition( 1321 | scale, 1322 | eventClientX(event), 1323 | eventClientY(event) 1324 | ); 1325 | _this._transform(); 1326 | _this.direction *= -1; 1327 | }); 1328 | } 1329 | }, 1330 | /** 1331 | * @private 1332 | */ 1333 | _prepare: function _prepare() { 1334 | var viewport = this.viewport, 1335 | content = this.content, 1336 | options = this.options; 1337 | var _getElementPosition = getElementPosition(viewport.$element), 1338 | left = _getElementPosition.left, 1339 | top = _getElementPosition.top; 1340 | viewport.originalWidth = viewport.$element.offsetWidth; 1341 | viewport.originalHeight = viewport.$element.offsetHeight; 1342 | viewport.originalLeft = left; 1343 | viewport.originalTop = top; 1344 | if (options.type === 'image') { 1345 | content.originalWidth = 1346 | options.width || content.$element.naturalWidth; 1347 | content.originalHeight = 1348 | options.height || content.$element.naturalHeight; 1349 | } else { 1350 | content.originalWidth = 1351 | options.width || content.$element.offsetWidth; 1352 | content.originalHeight = 1353 | options.height || content.$element.offsetHeight; 1354 | } 1355 | content.maxScale = options.maxScale; 1356 | content.minScale = 1357 | options.minScale || 1358 | Math.min( 1359 | viewport.originalWidth / content.originalWidth, 1360 | viewport.originalHeight / content.originalHeight, 1361 | content.maxScale 1362 | ); 1363 | content.currentScale = content.minScale; 1364 | content.currentWidth = content.originalWidth * content.currentScale; 1365 | content.currentHeight = 1366 | content.originalHeight * content.currentScale; 1367 | var _calculateAlignPoint = calculateAlignPoint( 1368 | viewport, 1369 | content, 1370 | options.alignContent 1371 | ); 1372 | var _calculateAlignPoint2 = _slicedToArray(_calculateAlignPoint, 2); 1373 | content.alignPointX = _calculateAlignPoint2[0]; 1374 | content.alignPointY = _calculateAlignPoint2[1]; 1375 | content.currentLeft = content.alignPointX; 1376 | content.currentTop = content.alignPointY; 1377 | 1378 | // calculate indent-left and indent-top to of content from viewport borders 1379 | var _calculateCorrectPoin = calculateCorrectPoint( 1380 | viewport, 1381 | content, 1382 | options.alignContent 1383 | ); 1384 | var _calculateCorrectPoin2 = _slicedToArray( 1385 | _calculateCorrectPoin, 1386 | 2 1387 | ); 1388 | content.correctX = _calculateCorrectPoin2[0]; 1389 | content.correctY = _calculateCorrectPoin2[1]; 1390 | if (typeof options.prepare === 'function') { 1391 | options.prepare(this); 1392 | } 1393 | this._transform(); 1394 | }, 1395 | /** 1396 | * @private 1397 | */ 1398 | _computeScale: function _computeScale(direction) { 1399 | this.direction = direction < 0 ? 1 : -1; 1400 | var _this$content = this.content, 1401 | minScale = _this$content.minScale, 1402 | maxScale = _this$content.maxScale, 1403 | currentScale = _this$content.currentScale; 1404 | var scale = 1405 | currentScale * Math.pow(this.options.speed, this.direction); 1406 | if (scale <= minScale) { 1407 | this.direction = 1; 1408 | return minScale; 1409 | } 1410 | if (scale >= maxScale) { 1411 | this.direction = -1; 1412 | return maxScale; 1413 | } 1414 | return scale; 1415 | }, 1416 | /** 1417 | * @param {number} scale 1418 | * @param {number} x 1419 | * @param {number} y 1420 | * @private 1421 | */ 1422 | _computePosition: function _computePosition(scale, x, y) { 1423 | var viewport = this.viewport, 1424 | content = this.content, 1425 | options = this.options, 1426 | direction = this.direction; 1427 | var contentNewWidth = content.originalWidth * scale; 1428 | var contentNewHeight = content.originalHeight * scale; 1429 | var scrollLeft = getPageScrollLeft(); 1430 | var scrollTop = getPageScrollTop(); 1431 | 1432 | // calculate the parameters along the X axis 1433 | var contentNewLeft = calculateContentShift( 1434 | x, 1435 | scrollLeft, 1436 | viewport.originalLeft, 1437 | content.currentLeft, 1438 | viewport.originalWidth, 1439 | contentNewWidth / content.currentWidth 1440 | ); 1441 | // calculate the parameters along the Y axis 1442 | var contentNewTop = calculateContentShift( 1443 | y, 1444 | scrollTop, 1445 | viewport.originalTop, 1446 | content.currentTop, 1447 | viewport.originalHeight, 1448 | contentNewHeight / content.currentHeight 1449 | ); 1450 | if (direction === -1) { 1451 | // check that the content does not go beyond the X axis 1452 | contentNewLeft = calculateContentMaxShift( 1453 | options.alignContent, 1454 | viewport.originalWidth, 1455 | content.correctX, 1456 | contentNewWidth, 1457 | contentNewLeft 1458 | ); 1459 | // check that the content does not go beyond the Y axis 1460 | contentNewTop = calculateContentMaxShift( 1461 | options.alignContent, 1462 | viewport.originalHeight, 1463 | content.correctY, 1464 | contentNewHeight, 1465 | contentNewTop 1466 | ); 1467 | } 1468 | if (scale === content.minScale) { 1469 | contentNewLeft = content.alignPointX; 1470 | contentNewTop = content.alignPointY; 1471 | } 1472 | content.currentWidth = contentNewWidth; 1473 | content.currentHeight = contentNewHeight; 1474 | content.currentLeft = contentNewLeft; 1475 | content.currentTop = contentNewTop; 1476 | content.currentScale = scale; 1477 | }, 1478 | /** 1479 | * @param {number} smoothTime 1480 | * @private 1481 | */ 1482 | _transform: function _transform(smoothTime) { 1483 | if (smoothTime === undefined) smoothTime = this.options.smoothTime; 1484 | transition(this.content.$element, smoothTime); 1485 | transform( 1486 | this.content.$element, 1487 | this.content.currentLeft, 1488 | this.content.currentTop, 1489 | this.content.currentScale 1490 | ); 1491 | if (typeof this.options.rescale === 'function') { 1492 | this.options.rescale(this); 1493 | } 1494 | }, 1495 | /** 1496 | * todo добавить проверку на то что бы переданные координаты не выходили за пределы возможного 1497 | * @param {number} scale 1498 | * @param {Object} coordinates 1499 | * @private 1500 | */ 1501 | _zoom: function _zoom(scale) { 1502 | var coordinates = 1503 | arguments.length > 1 && arguments[1] !== undefined 1504 | ? arguments[1] 1505 | : {}; 1506 | // if the coordinates are not passed, then use the coordinates of the center 1507 | if (coordinates.x === undefined || coordinates.y === undefined) { 1508 | coordinates = calculateViewportCenter(this.viewport); 1509 | } 1510 | this._computePosition(scale, coordinates.x, coordinates.y); 1511 | this._transform(); 1512 | }, 1513 | _destroyObservers: function _destroyObservers() { 1514 | var _iterator = _createForOfIteratorHelper(this.observers), 1515 | _step; 1516 | try { 1517 | for (_iterator.s(); !(_step = _iterator.n()).done; ) { 1518 | var observer = _step.value; 1519 | observer.destroy(); 1520 | } 1521 | } catch (err) { 1522 | _iterator.e(err); 1523 | } finally { 1524 | _iterator.f(); 1525 | } 1526 | }, 1527 | prepare: function prepare() { 1528 | this._prepare(); 1529 | }, 1530 | /** 1531 | * todo добавить проверку на то что бы переданный state вообще возможен для данного instance 1532 | * @param {number} top 1533 | * @param {number} left 1534 | * @param {number} scale 1535 | */ 1536 | transform: function transform(top, left, scale) { 1537 | var content = this.content; 1538 | content.currentWidth = content.originalWidth * scale; 1539 | content.currentHeight = content.originalHeight * scale; 1540 | content.currentLeft = left; 1541 | content.currentTop = top; 1542 | content.currentScale = scale; 1543 | this._transform(); 1544 | }, 1545 | zoomUp: function zoomUp() { 1546 | this._zoom(this._computeScale(-1)); 1547 | }, 1548 | zoomDown: function zoomDown() { 1549 | this._zoom(this._computeScale(1)); 1550 | }, 1551 | maxZoomUp: function maxZoomUp() { 1552 | this._zoom(this.content.maxScale); 1553 | }, 1554 | maxZoomDown: function maxZoomDown() { 1555 | this._zoom(this.content.minScale); 1556 | }, 1557 | zoomUpToPoint: function zoomUpToPoint(coordinates) { 1558 | this._zoom(this._computeScale(-1), coordinates); 1559 | }, 1560 | zoomDownToPoint: function zoomDownToPoint(coordinates) { 1561 | this._zoom(this._computeScale(1), coordinates); 1562 | }, 1563 | maxZoomUpToPoint: function maxZoomUpToPoint(coordinates) { 1564 | this._zoom(this.content.maxScale, coordinates); 1565 | }, 1566 | destroy: function destroy() { 1567 | this.content.$element.style.removeProperty('transition'); 1568 | this.content.$element.style.removeProperty('transform'); 1569 | if (this.options.type === 'image') { 1570 | off(this.content.$element, 'load', this._init); 1571 | } 1572 | this._destroyObservers(); 1573 | for (var key in this) { 1574 | if (this.hasOwnProperty(key)) { 1575 | this[key] = null; 1576 | } 1577 | } 1578 | }, 1579 | }; 1580 | 1581 | /** 1582 | * @param {?WZoomOptions} targetOptions 1583 | * @param {?WZoomOptions} defaultOptions 1584 | * @returns {?WZoomOptions} 1585 | */ 1586 | function optionsConstructor(targetOptions, defaultOptions) { 1587 | var options = Object.assign({}, defaultOptions, targetOptions); 1588 | if (isTouch()) { 1589 | options.smoothTime = 0; 1590 | options.smoothTimeDrag = 0; 1591 | } else { 1592 | var smoothTime = Number(options.smoothTime); 1593 | var smoothTimeDrag = Number(options.smoothTimeDrag); 1594 | options.smoothTime = !isNaN(smoothTime) 1595 | ? smoothTime 1596 | : wZoomDefaultOptions.smoothTime; 1597 | options.smoothTimeDrag = !isNaN(smoothTimeDrag) 1598 | ? smoothTimeDrag 1599 | : options.smoothTime; 1600 | } 1601 | return options; 1602 | } 1603 | 1604 | /** 1605 | * Create WZoom instance 1606 | * @param {string|HTMLElement} selectorOrHTMLElement 1607 | * @param {WZoomOptions} [options] 1608 | * @returns {WZoom} 1609 | */ 1610 | WZoom.create = function (selectorOrHTMLElement) { 1611 | var options = 1612 | arguments.length > 1 && arguments[1] !== undefined 1613 | ? arguments[1] 1614 | : {}; 1615 | return new WZoom(selectorOrHTMLElement, options); 1616 | }; 1617 | 1618 | /** 1619 | * @typedef WZoomContent 1620 | * @type {Object} 1621 | * @property {HTMLElement} [$element] 1622 | * @property {number} [originalWidth] 1623 | * @property {number} [originalHeight] 1624 | * @property {number} [currentWidth] 1625 | * @property {number} [currentHeight] 1626 | * @property {number} [currentLeft] 1627 | * @property {number} [currentTop] 1628 | * @property {number} [currentScale] 1629 | * @property {number} [maxScale] 1630 | * @property {number} [minScale] 1631 | * @property {number} [alignPointX] 1632 | * @property {number} [alignPointY] 1633 | * @property {number} [correctX] 1634 | * @property {number} [correctY] 1635 | */ 1636 | 1637 | /** 1638 | * @typedef WZoomViewport 1639 | * @type {Object} 1640 | * @property {HTMLElement} [$element] 1641 | * @property {number} [originalWidth] 1642 | * @property {number} [originalHeight] 1643 | * @property {number} [originalLeft] 1644 | * @property {number} [originalTop] 1645 | */ 1646 | 1647 | return WZoom; 1648 | }); 1649 | -------------------------------------------------------------------------------- /dist/wheel-zoom.min.js: -------------------------------------------------------------------------------- 1 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).WZoom=e()}(this,(function(){"use strict";function t(t,e){(null==e||e>t.length)&&(e=t.length);for(var n=0,o=Array(e);n=t.length?{done:!0}:{done:!1,value:t[o++]}},e:function(t){throw t},f:i}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var r,s=!0,c=!1;return{s:function(){n=n.call(t)},n:function(){var t=n.next();return s=t.done,t},e:function(t){c=!0,r=t},f:function(){try{s||null==n.return||n.return()}finally{if(c)throw r}}}}function r(t,e,n){return(e=m(e))in t?Object.defineProperty(t,e,{value:n,enumerable:!0,configurable:!0,writable:!0}):t[e]=n,t}function s(){return s="undefined"!=typeof Reflect&&Reflect.get?Reflect.get.bind():function(t,e,n){var o=function(t,e){for(;!{}.hasOwnProperty.call(t,e)&&null!==(t=c(t)););return t}(t,e);if(o){var i=Object.getOwnPropertyDescriptor(o,e);return i.get?i.get.call(arguments.length<3?t:n):i.value}},s.apply(null,arguments)}function c(t){return c=Object.setPrototypeOf?Object.getPrototypeOf.bind():function(t){return t.__proto__||Object.getPrototypeOf(t)},c(t)}function a(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function");t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,writable:!0,configurable:!0}}),Object.defineProperty(t,"prototype",{writable:!1}),e&&f(t,e)}function u(){try{var t=!Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],(function(){})))}catch(t){}return(u=function(){return!!t})()}function l(t,e){var n=Object.keys(t);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(t);e&&(o=o.filter((function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable}))),n.push.apply(n,o)}return n}function h(t){for(var e=1;e3&&void 0!==arguments[3]&&arguments[3];t.addEventListener(e,n,o)}function w(t,e,n){var o=arguments.length>3&&void 0!==arguments[3]&&arguments[3];t.removeEventListener(e,n,o)}function H(){return"ontouchstart"in window||navigator.MaxTouchPoints>0||navigator.msMaxTouchPoints>0}function T(t){return"wheel"===t.type||"pointerup"===t.type||"pointerdown"===t.type||"pointermove"===t.type||"mousedown"===t.type||"mousemove"===t.type||"mouseup"===t.type?t.clientX:t.changedTouches[0].clientX}function O(t){return"wheel"===t.type||"pointerup"===t.type||"pointerdown"===t.type||"pointermove"===t.type||"mousedown"===t.type||"mousemove"===t.type||"mouseup"===t.type?t.clientY:t.changedTouches[0].clientY}function S(t,e,n,o,i,r){var s=i/2-(t+e-n)+o;return s*r-s+o}function P(t,e,n,o,i){switch(t){case"left":o/2-i1)return!1;var e=this.coordinatesShift,n=this.coordinates;e.x=T(t)-n.x,e.y=O(t)-n.y,n.x=T(t),n.y=O(t),clearTimeout(this.moveTimer),this.moveTimer=setTimeout((function(){e.x=0,e.y=0}),50),t.data=h(h({},t.data||{}),{},{x:e.x,y:e.y}),this._run(x,t)}}])}(D),E="click",$="dblclick",z="wheel",C=function(t){function i(t){var o;return n(this,i),(o=e(this,i)).target=t,o.coordsOnDown=null,o.pressingTimeout=null,o.firstClick=!0,o.isTouch=H(),o.events=o.isTouch?{down:"touchstart",up:"touchend"}:{down:"mousedown",up:"mouseup"},o.events.options=!!o.isTouch&&{passive:!0},o._downHandler=o._downHandler.bind(o),o._upHandler=o._upHandler.bind(o),o._wheelHandler=o._wheelHandler.bind(o),_(o.target,o.events.down,o._downHandler,o.events.options),_(o.target,o.events.up,o._upHandler,o.events.options),_(o.target,z,o._wheelHandler),o}return a(i,t),o(i,[{key:"destroy",value:function(){w(this.target,this.events.down,this._downHandler,this.events.options),w(this.target,this.events.up,this._upHandler,this.events.options),w(this.target,z,this._wheelHandler,this.events.options),p(i,"destroy",this)([])}},{key:"_downHandler",value:function(t){this.coordsOnDown=null,(this.isTouch&&1===t.touches.length||1===t.buttons)&&(this.coordsOnDown={x:T(t),y:O(t)}),clearTimeout(this.pressingTimeout)}},{key:"_upHandler",value:function(t){var e=this,n=this.subscribes[$]?setTimeout:function(t,e){return t()};this.firstClick?(this.firstClick=!1,this.pressingTimeout=n((function(){e._isDetectedShift(t)||e._run(E,t),e.firstClick=!0}),200)):this.pressingTimeout=n((function(){e._isDetectedShift(t)||e._run($,t),e.firstClick=!0}),100)}},{key:"_wheelHandler",value:function(t){this._run(z,t)}},{key:"_isDetectedShift",value:function(t){return!(this.coordsOnDown&&this.coordsOnDown.x===T(t)&&this.coordsOnDown.y===O(t))}}])}(D),L="pinchtozoom",Y=function(t){function i(t){var o;return n(this,i),(o=e(this,i)).target=t,o.fingersHypot=null,o.zoomPinchWasDetected=!1,o._touchMoveHandler=o._touchMoveHandler.bind(o),o._touchEndHandler=o._touchEndHandler.bind(o),_(o.target,"touchmove",o._touchMoveHandler),_(o.target,"touchend",o._touchEndHandler),o}return a(i,t),o(i,[{key:"destroy",value:function(){w(this.target,"touchmove",this._touchMoveHandler),w(this.target,"touchend",this._touchEndHandler),p(i,"destroy",this)([])}},{key:"_touchMoveHandler",value:function(t){if(2===t.targetTouches.length){var e=t.targetTouches[0].clientX,n=t.targetTouches[0].clientY,o=t.targetTouches[1].clientX,i=t.targetTouches[1].clientY,r=Math.round(Math.sqrt(Math.pow(Math.abs(e-o),2)+Math.pow(Math.abs(n-i),2))),s=0;if(r>this.fingersHypot+5&&(s=-1),r1&&void 0!==arguments[1]?arguments[1]:{};if(this._init=this._init.bind(this),this._prepare=this._prepare.bind(this),this._computeScale=this._computeScale.bind(this),this._computePosition=this._computePosition.bind(this),this._transform=this._transform.bind(this),this.content={},"string"==typeof t){if(this.content.$element=document.querySelector(t),!this.content.$element)throw"WZoom: Element with selector `".concat(t,"` not found")}else{if(!(t instanceof HTMLElement))throw"WZoom: `selectorOrHTMLElement` must be selector or HTMLElement, and not ".concat({}.toString.call(t));this.content.$element=t}this.viewport={},this.viewport.$element=this.content.$element.parentElement,this.options=function(t,e){var n=Object.assign({},e,t);if(H())n.smoothTime=0,n.smoothTimeDrag=0;else{var o=Number(n.smoothTime),i=Number(n.smoothTimeDrag);n.smoothTime=isNaN(o)?k.smoothTime:o,n.smoothTimeDrag=isNaN(i)?n.smoothTime:i}return n}(e,k),this.isTouch=H(),this.direction=1,this.observers=[],"image"===this.options.type?this.content.$element.complete?this._init():_(this.content.$element,"load",this._init,{once:!0}):this._init()}return X.prototype={constructor:X,_init:function(){var t=this,e=this.viewport,n=this.content,o=this.options,i=this.observers;if(this._prepare(),this._destroyObservers(),!0===o.dragScrollable){var r=new M(n.$element);i.push(r),"function"==typeof o.onGrab&&r.on(W,(function(e){e.preventDefault(),o.onGrab(e,t)})),"function"==typeof o.onDrop&&r.on(j,(function(e){e.preventDefault(),o.onDrop(e,t)})),r.on(x,(function(i){i.preventDefault();var r=i.data,s=r.x,c=r.y,a=n.currentLeft+s,u=n.currentTop+c,l=(n.currentWidth-e.originalWidth)/2+n.correctX,h=(n.currentHeight-e.originalHeight)/2+n.correctY;Math.abs(a)<=l&&(n.currentLeft=a),Math.abs(u)<=h&&(n.currentTop=u),t._transform(o.smoothTimeDrag),"function"==typeof o.onMove&&o.onMove(i,t)}))}var s=new C(n.$element);if(i.push(s),!o.disableWheelZoom)if(this.isTouch){var c=new Y(n.$element);i.push(c),c.on(L,(function(e){var n=e.data,o=n.clientX,i=n.clientY,r=n.direction,s=t._computeScale(r);t._computePosition(s,o,i),t._transform()}))}else s.on(z,(function(e){e.preventDefault();var n=o.reverseWheelDirection?-e.deltaY:e.deltaY,i=t._computeScale(n);t._computePosition(i,T(e),O(e)),t._transform()}));if(o.zoomOnClick||o.zoomOnDblClick){var a=o.zoomOnDblClick?$:E;s.on(a,(function(e){var o=1===t.direction?n.maxScale:n.minScale;t._computePosition(o,T(e),O(e)),t._transform(),t.direction*=-1}))}},_prepare:function(){var t=this.viewport,e=this.content,n=this.options,o=g(t.$element),i=o.left,r=o.top;t.originalWidth=t.$element.offsetWidth,t.originalHeight=t.$element.offsetHeight,t.originalLeft=i,t.originalTop=r,"image"===n.type?(e.originalWidth=n.width||e.$element.naturalWidth,e.originalHeight=n.height||e.$element.naturalHeight):(e.originalWidth=n.width||e.$element.offsetWidth,e.originalHeight=n.height||e.$element.offsetHeight),e.maxScale=n.maxScale,e.minScale=n.minScale||Math.min(t.originalWidth/e.originalWidth,t.originalHeight/e.originalHeight,e.maxScale),e.currentScale=e.minScale,e.currentWidth=e.originalWidth*e.currentScale,e.currentHeight=e.originalHeight*e.currentScale;var s=function(t,e,n){var o=0,i=0;switch(n){case"top":i=(e.currentHeight-t.originalHeight)/2;break;case"right":o=(e.currentWidth-t.originalWidth)/2*-1;break;case"bottom":i=(e.currentHeight-t.originalHeight)/2*-1;break;case"left":o=(e.currentWidth-t.originalWidth)/2}return[o,i]}(t,e,n.alignContent),c=d(s,2);e.alignPointX=c[0],e.alignPointY=c[1],e.currentLeft=e.alignPointX,e.currentTop=e.alignPointY;var a=function(t,e,n){var o=Math.max(0,(t.originalWidth-e.currentWidth)/2),i=Math.max(0,(t.originalHeight-e.currentHeight)/2);switch(n){case"top":i=0;break;case"right":o=0;break;case"bottom":i*=2;break;case"left":o*=2}return[o,i]}(t,e,n.alignContent),u=d(a,2);e.correctX=u[0],e.correctY=u[1],"function"==typeof n.prepare&&n.prepare(this),this._transform()},_computeScale:function(t){this.direction=t<0?1:-1;var e=this.content,n=e.minScale,o=e.maxScale,i=e.currentScale*Math.pow(this.options.speed,this.direction);return i<=n?(this.direction=1,n):i>=o?(this.direction=-1,o):i},_computePosition:function(t,e,n){var o=this.viewport,i=this.content,r=this.options,s=this.direction,c=i.originalWidth*t,a=i.originalHeight*t,u=y(),l=b(),h=S(e,u,o.originalLeft,i.currentLeft,o.originalWidth,c/i.currentWidth),f=S(n,l,o.originalTop,i.currentTop,o.originalHeight,a/i.currentHeight);-1===s&&(h=P(r.alignContent,o.originalWidth,i.correctX,c,h),f=P(r.alignContent,o.originalHeight,i.correctY,a,f)),t===i.minScale&&(h=i.alignPointX,f=i.alignPointY),i.currentWidth=c,i.currentHeight=a,i.currentLeft=h,i.currentTop=f,i.currentScale=t},_transform:function(t){var e,n;void 0===t&&(t=this.options.smoothTime),e=this.content.$element,(n=t)?e.style.transition="transform ".concat(n,"s"):e.style.removeProperty("transition"),function(t,e,n,o){t.style.transform="translate(".concat(e,"px, ").concat(n,"px) scale(").concat(o,")")}(this.content.$element,this.content.currentLeft,this.content.currentTop,this.content.currentScale),"function"==typeof this.options.rescale&&this.options.rescale(this)},_zoom:function(t){var e,n,o=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};void 0!==o.x&&void 0!==o.y||(e=this.viewport,o={x:(n=g(e.$element)).left+e.originalWidth/2-y(),y:n.top+e.originalHeight/2-b()}),this._computePosition(t,o.x,o.y),this._transform()},_destroyObservers:function(){var t,e=i(this.observers);try{for(e.s();!(t=e.n()).done;){t.value.destroy()}}catch(t){e.e(t)}finally{e.f()}},prepare:function(){this._prepare()},transform:function(t,e,n){var o=this.content;o.currentWidth=o.originalWidth*n,o.currentHeight=o.originalHeight*n,o.currentLeft=e,o.currentTop=t,o.currentScale=n,this._transform()},zoomUp:function(){this._zoom(this._computeScale(-1))},zoomDown:function(){this._zoom(this._computeScale(1))},maxZoomUp:function(){this._zoom(this.content.maxScale)},maxZoomDown:function(){this._zoom(this.content.minScale)},zoomUpToPoint:function(t){this._zoom(this._computeScale(-1),t)},zoomDownToPoint:function(t){this._zoom(this._computeScale(1),t)},maxZoomUpToPoint:function(t){this._zoom(this.content.maxScale,t)},destroy:function(){for(var t in this.content.$element.style.removeProperty("transition"),this.content.$element.style.removeProperty("transform"),"image"===this.options.type&&w(this.content.$element,"load",this._init),this._destroyObservers(),this)this.hasOwnProperty(t)&&(this[t]=null)}},X.create=function(t){return new X(t,arguments.length>1&&void 0!==arguments[1]?arguments[1]:{})},X})); -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:latest 2 | RUN npm i -g npm-check-updates -------------------------------------------------------------------------------- /docker/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | IMAGE_NAME='vanilla-js-wheel-zoom/image' 4 | CONTAINER_NAME='vanilla-js-wheel-zoom' 5 | 6 | LOCAL_DIR="$(pwd)/.." 7 | WORK_DIR='/app' 8 | 9 | docker build --tag $IMAGE_NAME . 10 | docker run -itd --volume $LOCAL_DIR:$WORK_DIR --name $CONTAINER_NAME $IMAGE_NAME 11 | docker exec -it --workdir $WORK_DIR $CONTAINER_NAME sh 12 | -------------------------------------------------------------------------------- /examples/html.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | worka/vanilla-js-wheel-zoom 4 | 5 | 6 | 7 | 8 | 10 | 11 | 34 | 35 | 36 |
37 |
38 |
39 |

vanilla-js-wheel-zoom

40 | 41 |

42 | 43 | https://github.com/worka/vanilla-js-wheel-zoom 44 | 45 |

46 | 47 |
48 | 49 | 50 | 51 |
52 | 53 |
54 |
55 |
56 |
Bridge
57 | image 58 |
59 |
60 |
61 | 62 |
Move your mouse over the image and scroll to zoom in and out.
63 |
64 |
65 |
66 | 67 | 68 | 69 | 133 | 134 | 135 | -------------------------------------------------------------------------------- /examples/image-rotate.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | worka/vanilla-js-wheel-zoom 4 | 5 | 6 | 7 | 8 | 10 | 11 | 23 | 24 | 25 |
26 |
27 |
28 |

vanilla-js-wheel-zoom

29 | 30 |

31 | 32 | https://github.com/worka/vanilla-js-wheel-zoom 33 | 34 |

35 | 36 |
37 | 38 |
39 | 40 |
41 |
42 | image 43 |
44 |
45 |
46 |
47 |
48 | 49 | 50 | 51 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /examples/image.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | worka/vanilla-js-wheel-zoom 4 | 5 | 6 | 7 | 8 | 10 | 11 | 23 | 24 | 25 |
26 |
27 |
28 |

vanilla-js-wheel-zoom

29 | 30 |

31 | 32 | https://github.com/worka/vanilla-js-wheel-zoom 33 | 34 |

35 | 36 |
37 |
38 | 39 | 40 |
41 | 42 |
43 | 44 | 45 |
46 |
47 | 48 |
49 |
50 | image 51 |
52 |
53 | 54 |
55 | 56 | 57 |
58 | 59 |
Move your mouse over the image and scroll to zoom in and out.
60 |
61 |
62 |
63 | 64 | 65 | 66 | 142 | 143 | 144 | -------------------------------------------------------------------------------- /examples/images.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | worka/vanilla-js-wheel-zoom 4 | 5 | 6 | 7 | 8 | 10 | 11 | 32 | 33 | 34 |
35 |

vanilla-js-wheel-zoom

36 | 37 |

38 | 39 | https://github.com/worka/vanilla-js-wheel-zoom 40 | 41 |

42 | 43 |
44 |
45 |
46 |
47 |
Loading...
48 | image 49 |
50 |
51 |
52 | 53 |
54 |
55 |
56 |
Loading...
57 | image 58 |
59 |
60 |
61 | 62 |
63 |
64 |
65 |
Loading...
66 | image 67 |
68 |
69 |
70 | 71 |
72 |
73 |
74 |
Loading...
75 | image 76 |
77 |
78 |
79 |
80 |
81 | 82 | 83 | 84 | 139 | 140 | 141 | -------------------------------------------------------------------------------- /examples/scale-reached.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | worka/vanilla-js-wheel-zoom 4 | 5 | 6 | 7 | 8 | 10 | 11 | 19 | 20 | 21 |
22 |
23 |
24 |

vanilla-js-wheel-zoom

25 | 26 |

27 | 28 | https://github.com/worka/vanilla-js-wheel-zoom 29 | 30 |

31 | 32 |
33 | 34 |
35 | 36 |
37 |
38 | image 39 |
40 |
41 |
42 |
43 |
44 | 45 | 46 | 47 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'vanilla-js-wheel-zoom'; 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vanilla-js-wheel-zoom", 3 | "version": "9.0.4", 4 | "main": "src/wheel-zoom.js", 5 | "devDependencies": { 6 | "@babel/core": "^7.25.8", 7 | "@babel/preset-env": "^7.25.8", 8 | "@rollup/plugin-babel": "^6.0.4", 9 | "prettier": "3.3.3", 10 | "rollup": "^4.24.0", 11 | "terser": "^5.34.1" 12 | }, 13 | "scripts": { 14 | "build": "yarn rollup && yarn prettier && yarn minify", 15 | "watch": "yarn rollup --watch", 16 | "rollup": "node ./node_modules/rollup/dist/bin/rollup --config", 17 | "prettier": "node ./node_modules/prettier/bin/prettier.cjs --write dist/wheel-zoom.js", 18 | "minify": "node ./node_modules/terser/bin/terser dist/wheel-zoom.js -cm --output dist/wheel-zoom.min.js" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/worka/vanilla-js-wheel-zoom.git" 23 | }, 24 | "keywords": [ 25 | "wheelzoom", 26 | "js", 27 | "javascript", 28 | "vanilla", 29 | "mousewheel", 30 | "image zoom", 31 | "image scale", 32 | "drag scrollable image", 33 | "zoom", 34 | "pinch-to-zoom" 35 | ], 36 | "author": "worka", 37 | "license": "MIT", 38 | "bugs": { 39 | "url": "https://github.com/worka/vanilla-js-wheel-zoom/issues" 40 | }, 41 | "homepage": "https://github.com/worka/vanilla-js-wheel-zoom" 42 | } 43 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | bracketSpacing: true, 3 | trailingComma: 'es5', 4 | bracketSameLine: false, 5 | tabWidth: 4, 6 | semi: true, 7 | singleQuote: true, 8 | }; 9 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | const babel = require('@rollup/plugin-babel'); 2 | 3 | module.exports = { 4 | input: 'src/wheel-zoom.js', 5 | output: { 6 | file: 'dist/wheel-zoom.js', 7 | format: 'umd', 8 | name: 'WZoom' 9 | }, 10 | plugins: [ 11 | babel({ 12 | babelHelpers: 'bundled', 13 | }) 14 | ], 15 | watch: { 16 | exclude: 'node_modules/**', 17 | clearScreen: false, 18 | chokidar: { 19 | usePolling: true 20 | } 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /src/calculator.js: -------------------------------------------------------------------------------- 1 | import { getElementPosition, getPageScrollLeft, getPageScrollTop } from './toolkit'; 2 | 3 | /** 4 | * @param {WZoomViewport} viewport 5 | * @param {WZoomContent} content 6 | * @param {string} align 7 | * @returns {number[]} 8 | */ 9 | export function calculateAlignPoint(viewport, content, align) { 10 | let pointX = 0; 11 | let pointY = 0; 12 | 13 | switch (align) { 14 | case 'top': 15 | pointY = (content.currentHeight - viewport.originalHeight) / 2; 16 | break; 17 | case 'right': 18 | pointX = (content.currentWidth - viewport.originalWidth) / 2 * -1; 19 | break; 20 | case 'bottom': 21 | pointY = (content.currentHeight - viewport.originalHeight) / 2 * -1; 22 | break; 23 | case 'left': 24 | pointX = (content.currentWidth - viewport.originalWidth) / 2; 25 | break; 26 | } 27 | 28 | return [ pointX, pointY ]; 29 | } 30 | 31 | /** 32 | * @param {WZoomViewport} viewport 33 | * @param {WZoomContent} content 34 | * @param {string} align 35 | * @returns {number[]} 36 | */ 37 | export function calculateCorrectPoint(viewport, content, align) { 38 | let pointX = Math.max(0, (viewport.originalWidth - content.currentWidth) / 2); 39 | let pointY = Math.max(0, (viewport.originalHeight - content.currentHeight) / 2); 40 | 41 | switch (align) { 42 | case 'top': 43 | pointY = 0; 44 | break; 45 | case 'right': 46 | pointX = 0; 47 | break; 48 | case 'bottom': 49 | pointY = pointY * 2; 50 | break; 51 | case 'left': 52 | pointX = pointX * 2; 53 | break; 54 | } 55 | 56 | return [ pointX, pointY ]; 57 | } 58 | 59 | /** 60 | * @returns {number} 61 | */ 62 | export function calculateContentShift(axisValue, axisScroll, axisViewportPosition, axisContentPosition, originalViewportSize, contentSizeRatio) { 63 | const viewportShift = axisValue + axisScroll - axisViewportPosition; 64 | const centerViewportShift = originalViewportSize / 2 - viewportShift; 65 | const centerContentShift = centerViewportShift + axisContentPosition; 66 | 67 | return centerContentShift * contentSizeRatio - centerContentShift + axisContentPosition; 68 | } 69 | 70 | export function calculateContentMaxShift(align, originalViewportSize, correctCoordinate, size, shift) { 71 | switch (align) { 72 | case 'left': 73 | if (size / 2 - shift < originalViewportSize / 2) { 74 | shift = (size - originalViewportSize) / 2; 75 | } 76 | break; 77 | case 'right': 78 | if (size / 2 + shift < originalViewportSize / 2) { 79 | shift = (size - originalViewportSize) / 2 * -1; 80 | } 81 | break; 82 | default: 83 | if ((size - originalViewportSize) / 2 + correctCoordinate < Math.abs(shift)) { 84 | const positive = shift < 0 ? -1 : 1; 85 | shift = ((size - originalViewportSize) / 2 + correctCoordinate) * positive; 86 | } 87 | } 88 | 89 | return shift; 90 | } 91 | 92 | /** 93 | * @param {WZoomViewport} viewport 94 | * @returns {{x: number, y: number}} 95 | */ 96 | export function calculateViewportCenter(viewport) { 97 | const viewportPosition = getElementPosition(viewport.$element); 98 | 99 | return { 100 | x: viewportPosition.left + (viewport.originalWidth / 2) - getPageScrollLeft(), 101 | y: viewportPosition.top + (viewport.originalHeight / 2) - getPageScrollTop(), 102 | }; 103 | } 104 | -------------------------------------------------------------------------------- /src/default-options.js: -------------------------------------------------------------------------------- 1 | /** @type {WZoomOptions} */ 2 | export const wZoomDefaultOptions = { 3 | // type content: `image` - only one image, `html` - any HTML content 4 | type: 'image', 5 | // for type `image` computed auto (if width set null), for type `html` need set real html content width, else computed auto 6 | width: null, 7 | // for type `image` computed auto (if height set null), for type `html` need set real html content height, else computed auto 8 | height: null, 9 | // minimum allowed proportion of scale (computed auto if null) 10 | minScale: null, 11 | // maximum allowed proportion of scale (1 = 100% content size) 12 | maxScale: 1, 13 | // content resizing speed 14 | speed: 1.1, 15 | // zoom to maximum (minimum) size on click 16 | zoomOnClick: true, 17 | // zoom to maximum (minimum) size on double click 18 | zoomOnDblClick: false, 19 | // smooth extinction 20 | smoothTime: .25, 21 | // align content `center`, `left`, `top`, `right`, `bottom` 22 | alignContent: 'center', 23 | // ******************** // 24 | disableWheelZoom: false, 25 | // option to reverse wheel direction 26 | reverseWheelDirection: false, 27 | // ******************** // 28 | // drag scrollable content 29 | dragScrollable: true, 30 | }; 31 | 32 | /** 33 | * @typedef WZoomOptions 34 | * @type {Object} 35 | * @property {string} type 36 | * @property {?number} width 37 | * @property {?number} height 38 | * @property {?number} minScale 39 | * @property {number} maxScale 40 | * @property {number} speed 41 | * @property {boolean} zoomOnClick 42 | * @property {boolean} zoomOnDblClick 43 | * @property {number} smoothTime 44 | * @property {string} alignContent 45 | * @property {boolean} disableWheelZoom 46 | * @property {boolean} reverseWheelDirection 47 | * @property {boolean} dragScrollable 48 | * @property {number} smoothTimeDrag 49 | * @property {?Function} onGrab 50 | * @property {?Function} onMove 51 | * @property {?Function} onDrop 52 | */ 53 | -------------------------------------------------------------------------------- /src/observers/AbstractObserver.js: -------------------------------------------------------------------------------- 1 | class AbstractObserver { 2 | /** 3 | * @constructor 4 | */ 5 | constructor() { 6 | /** @type {Object void>} */ 7 | this.subscribes = {}; 8 | } 9 | 10 | /** 11 | * @param {string} eventType 12 | * @param {(event: Event) => void} eventHandler 13 | * @returns {AbstractObserver} 14 | */ 15 | on(eventType, eventHandler) { 16 | if (!(eventType in this.subscribes)) { 17 | this.subscribes[eventType] = []; 18 | } 19 | 20 | this.subscribes[eventType].push(eventHandler); 21 | 22 | return this; 23 | } 24 | 25 | destroy() { 26 | for (let key in this) { 27 | if (this.hasOwnProperty(key)) { 28 | this[key] = null; 29 | } 30 | } 31 | } 32 | 33 | /** 34 | * @param {string} eventType 35 | * @param {Event} event 36 | * @protected 37 | */ 38 | _run(eventType, event) { 39 | if (this.subscribes[eventType]) { 40 | for (const eventHandler of this.subscribes[eventType]) { 41 | eventHandler(event); 42 | } 43 | } 44 | } 45 | } 46 | 47 | export default AbstractObserver; 48 | -------------------------------------------------------------------------------- /src/observers/DragScrollableObserver.js: -------------------------------------------------------------------------------- 1 | import { on, off, eventClientX, eventClientY, isTouch } from '../toolkit'; 2 | import AbstractObserver from './AbstractObserver'; 3 | 4 | export const EVENT_GRAB = 'grab'; 5 | export const EVENT_MOVE = 'move'; 6 | export const EVENT_DROP = 'drop'; 7 | 8 | class DragScrollableObserver extends AbstractObserver { 9 | /** 10 | * @param {HTMLElement} target 11 | * @constructor 12 | */ 13 | constructor(target) { 14 | super(); 15 | 16 | this.target = target; 17 | 18 | this.moveTimer = null; 19 | this.coordinates = null; 20 | this.coordinatesShift = null; 21 | 22 | // check if we're using a touch screen 23 | this.isTouch = isTouch(); 24 | // switch to touch events if using a touch screen 25 | this.events = this.isTouch 26 | ? { grab: 'touchstart', move: 'touchmove', drop: 'touchend' } 27 | : { grab: 'mousedown', move: 'mousemove', drop: 'mouseup' }; 28 | // for the touch screen we set the parameter forcibly 29 | this.events.options = this.isTouch ? { passive: false } : false; 30 | 31 | this._dropHandler = this._dropHandler.bind(this); 32 | this._grabHandler = this._grabHandler.bind(this); 33 | this._moveHandler = this._moveHandler.bind(this); 34 | 35 | on(this.target, this.events.grab, this._grabHandler, this.events.options); 36 | } 37 | 38 | destroy() { 39 | off(this.target, this.events.grab, this._grabHandler, this.events.options); 40 | 41 | off(document, this.events.drop, this._dropHandler); 42 | off(document, this.events.move, this._moveHandler); 43 | 44 | super.destroy(); 45 | } 46 | 47 | /** 48 | * @param {Event|TouchEvent|MouseEvent} event 49 | * @private 50 | */ 51 | _grabHandler(event) { 52 | // if touch started (only one finger) or pressed left mouse button 53 | if ((this.isTouch && event.touches.length === 1) || event.buttons === 1) { 54 | this.coordinates = { x: eventClientX(event), y: eventClientY(event) }; 55 | this.coordinatesShift = { x: 0, y: 0 }; 56 | 57 | on(document, this.events.drop, this._dropHandler, this.events.options); 58 | on(document, this.events.move, this._moveHandler, this.events.options); 59 | 60 | this._run(EVENT_GRAB, event); 61 | } 62 | } 63 | 64 | /** 65 | * @param {Event} event 66 | * @private 67 | */ 68 | _dropHandler(event) { 69 | off(document, this.events.drop, this._dropHandler); 70 | off(document, this.events.move, this._moveHandler); 71 | 72 | this._run(EVENT_DROP, event); 73 | } 74 | 75 | /** 76 | * @param {Event|TouchEvent} event 77 | * @private 78 | */ 79 | _moveHandler(event) { 80 | // so that it does not move when the touch screen and more than one finger 81 | if (this.isTouch && event.touches.length > 1) return false; 82 | 83 | const { coordinatesShift, coordinates } = this; 84 | 85 | // change of the coordinate of the mouse cursor along the X/Y axis 86 | coordinatesShift.x = eventClientX(event) - coordinates.x; 87 | coordinatesShift.y = eventClientY(event) - coordinates.y; 88 | 89 | coordinates.x = eventClientX(event); 90 | coordinates.y = eventClientY(event); 91 | 92 | clearTimeout(this.moveTimer); 93 | 94 | // reset shift if cursor stops 95 | this.moveTimer = setTimeout(() => { 96 | coordinatesShift.x = 0; 97 | coordinatesShift.y = 0; 98 | }, 50); 99 | 100 | event.data = { ...event.data || {}, x: coordinatesShift.x, y: coordinatesShift.y }; 101 | 102 | this._run(EVENT_MOVE, event); 103 | } 104 | } 105 | 106 | export default DragScrollableObserver; 107 | -------------------------------------------------------------------------------- /src/observers/InteractionObserver.js: -------------------------------------------------------------------------------- 1 | import { eventClientX, eventClientY, isTouch, off, on } from '../toolkit'; 2 | import AbstractObserver from './AbstractObserver'; 3 | 4 | export const EVENT_CLICK = 'click'; 5 | export const EVENT_DBLCLICK = 'dblclick'; 6 | export const EVENT_WHEEL = 'wheel'; 7 | 8 | class InteractionObserver extends AbstractObserver { 9 | /** 10 | * @param {HTMLElement} target 11 | * @constructor 12 | */ 13 | constructor(target) { 14 | super(); 15 | 16 | this.target = target; 17 | 18 | this.coordsOnDown = null; 19 | this.pressingTimeout = null; 20 | this.firstClick = true; 21 | 22 | // check if we're using a touch screen 23 | this.isTouch = isTouch(); 24 | // switch to touch events if using a touch screen 25 | this.events = this.isTouch 26 | ? { down: 'touchstart', up: 'touchend' } 27 | : { down: 'mousedown', up: 'mouseup' }; 28 | // if using touch screen tells the browser that the default action will not be undone 29 | this.events.options = this.isTouch ? { passive: true } : false; 30 | 31 | this._downHandler = this._downHandler.bind(this); 32 | this._upHandler = this._upHandler.bind(this); 33 | this._wheelHandler = this._wheelHandler.bind(this); 34 | 35 | on(this.target, this.events.down, this._downHandler, this.events.options); 36 | on(this.target, this.events.up, this._upHandler, this.events.options); 37 | on(this.target, EVENT_WHEEL, this._wheelHandler); 38 | } 39 | 40 | destroy() { 41 | off(this.target, this.events.down, this._downHandler, this.events.options); 42 | off(this.target, this.events.up, this._upHandler, this.events.options); 43 | off(this.target, EVENT_WHEEL, this._wheelHandler, this.events.options); 44 | 45 | super.destroy(); 46 | } 47 | 48 | /** 49 | * @param {TouchEvent|MouseEvent|PointerEvent} event 50 | * @private 51 | */ 52 | _downHandler(event) { 53 | this.coordsOnDown = null; 54 | 55 | if ((this.isTouch && event.touches.length === 1) || event.buttons === 1) { 56 | this.coordsOnDown = { x: eventClientX(event), y: eventClientY(event) }; 57 | } 58 | 59 | clearTimeout(this.pressingTimeout); 60 | } 61 | 62 | /** 63 | * @param {TouchEvent|MouseEvent|PointerEvent} event 64 | * @private 65 | */ 66 | _upHandler(event) { 67 | const delay = 200; 68 | const setTimeoutInner = this.subscribes[EVENT_DBLCLICK] 69 | ? setTimeout 70 | : (cb, delay) => cb(); 71 | 72 | if (this.firstClick) { 73 | this.firstClick = false; 74 | 75 | this.pressingTimeout = setTimeoutInner(() => { 76 | if (!this._isDetectedShift(event)) { 77 | this._run(EVENT_CLICK, event); 78 | } 79 | 80 | this.firstClick = true; 81 | }, delay); 82 | } else { 83 | this.pressingTimeout = setTimeoutInner(() => { 84 | if (!this._isDetectedShift(event)) { 85 | this._run(EVENT_DBLCLICK, event); 86 | } 87 | 88 | this.firstClick = true; 89 | }, delay / 2); 90 | } 91 | } 92 | 93 | /** 94 | * @param {WheelEvent} event 95 | * @private 96 | */ 97 | _wheelHandler(event) { 98 | this._run(EVENT_WHEEL, event); 99 | } 100 | 101 | /** 102 | * @param {TouchEvent|MouseEvent|PointerEvent} event 103 | * @return {boolean} 104 | * @private 105 | */ 106 | _isDetectedShift(event) { 107 | return !(this.coordsOnDown && 108 | this.coordsOnDown.x === eventClientX(event) && 109 | this.coordsOnDown.y === eventClientY(event)); 110 | } 111 | } 112 | 113 | export default InteractionObserver; 114 | -------------------------------------------------------------------------------- /src/observers/PinchToZoomObserver.js: -------------------------------------------------------------------------------- 1 | import { off, on } from '../toolkit'; 2 | import AbstractObserver from './AbstractObserver'; 3 | 4 | export const EVENT_PINCH_TO_ZOOM = 'pinchtozoom'; 5 | 6 | const SHIFT_DECIDE_THAT_MOVE_STARTED = 5; 7 | 8 | class PinchToZoomObserver extends AbstractObserver { 9 | /** 10 | * @param {HTMLElement} target 11 | * @constructor 12 | */ 13 | constructor(target) { 14 | super(); 15 | 16 | this.target = target; 17 | 18 | this.fingersHypot = null; 19 | this.zoomPinchWasDetected = false; 20 | 21 | this._touchMoveHandler = this._touchMoveHandler.bind(this); 22 | this._touchEndHandler = this._touchEndHandler.bind(this); 23 | 24 | on(this.target, 'touchmove', this._touchMoveHandler); 25 | on(this.target, 'touchend', this._touchEndHandler); 26 | } 27 | 28 | destroy() { 29 | off(this.target, 'touchmove', this._touchMoveHandler); 30 | off(this.target, 'touchend', this._touchEndHandler); 31 | 32 | super.destroy(); 33 | } 34 | 35 | /** 36 | * @param {TouchEvent|PointerEvent} event 37 | * @private 38 | */ 39 | _touchMoveHandler(event) { 40 | // detect two fingers 41 | if (event.targetTouches.length === 2) { 42 | const pageX1 = event.targetTouches[0].clientX; 43 | const pageY1 = event.targetTouches[0].clientY; 44 | 45 | const pageX2 = event.targetTouches[1].clientX; 46 | const pageY2 = event.targetTouches[1].clientY; 47 | 48 | // Math.hypot() analog 49 | const fingersHypotNew = Math.round(Math.sqrt( 50 | Math.pow(Math.abs(pageX1 - pageX2), 2) + 51 | Math.pow(Math.abs(pageY1 - pageY2), 2) 52 | )); 53 | 54 | let direction = 0; 55 | if (fingersHypotNew > this.fingersHypot + SHIFT_DECIDE_THAT_MOVE_STARTED) direction = -1; 56 | if (fingersHypotNew < this.fingersHypot - SHIFT_DECIDE_THAT_MOVE_STARTED) direction = 1; 57 | 58 | if (direction !== 0) { 59 | if (this.fingersHypot !== null || direction === 1) { 60 | // middle position between fingers 61 | const clientX = Math.min(pageX1, pageX2) + (Math.abs(pageX1 - pageX2) / 2); 62 | const clientY = Math.min(pageY1, pageY2) + (Math.abs(pageY1 - pageY2) / 2); 63 | 64 | event.data = { ...event.data || {}, clientX, clientY, direction }; 65 | 66 | this._run(EVENT_PINCH_TO_ZOOM, event); 67 | } 68 | 69 | this.fingersHypot = fingersHypotNew; 70 | this.zoomPinchWasDetected = true; 71 | } 72 | } 73 | } 74 | 75 | /** 76 | * @private 77 | */ 78 | _touchEndHandler() { 79 | if (this.zoomPinchWasDetected) { 80 | this.fingersHypot = null; 81 | this.zoomPinchWasDetected = false; 82 | } 83 | } 84 | } 85 | 86 | export default PinchToZoomObserver; 87 | -------------------------------------------------------------------------------- /src/toolkit.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Get element position (with support old browsers) 3 | * @param {Element} element 4 | * @returns {{top: number, left: number}} 5 | */ 6 | export function getElementPosition(element) { 7 | const box = element.getBoundingClientRect(); 8 | 9 | const { body, documentElement } = document; 10 | 11 | const scrollTop = getPageScrollTop(); 12 | const scrollLeft = getPageScrollLeft(); 13 | 14 | const clientTop = documentElement.clientTop || body.clientTop || 0; 15 | const clientLeft = documentElement.clientLeft || body.clientLeft || 0; 16 | 17 | const top = box.top + scrollTop - clientTop; 18 | const left = box.left + scrollLeft - clientLeft; 19 | 20 | return { top, left }; 21 | } 22 | 23 | /** 24 | * Get page scroll left 25 | * @returns {number} 26 | */ 27 | export function getPageScrollLeft() { 28 | const supportPageOffset = window.pageXOffset !== undefined; 29 | const isCSS1Compat = ((document.compatMode || '') === 'CSS1Compat'); 30 | 31 | return supportPageOffset ? window.pageXOffset : isCSS1Compat ? document.documentElement.scrollLeft : document.body.scrollLeft; 32 | } 33 | 34 | /** 35 | * Get page scroll top 36 | * @returns {number} 37 | */ 38 | export function getPageScrollTop() { 39 | const supportPageOffset = window.pageYOffset !== undefined; 40 | const isCSS1Compat = ((document.compatMode || '') === 'CSS1Compat'); 41 | 42 | return supportPageOffset ? window.pageYOffset : isCSS1Compat ? document.documentElement.scrollTop : document.body.scrollTop; 43 | } 44 | 45 | /** 46 | * @param target 47 | * @param type 48 | * @param listener 49 | * @param options 50 | */ 51 | export function on(target, type, listener, options = false) { 52 | target.addEventListener(type, listener, options); 53 | } 54 | 55 | /** 56 | * @param target 57 | * @param type 58 | * @param listener 59 | * @param options 60 | */ 61 | export function off(target, type, listener, options = false) { 62 | target.removeEventListener(type, listener, options); 63 | } 64 | 65 | /** 66 | * @returns {boolean} 67 | */ 68 | export function isTouch() { 69 | return 'ontouchstart' in window || navigator.MaxTouchPoints > 0 || navigator.msMaxTouchPoints > 0; 70 | } 71 | 72 | /** 73 | * @param {Event} event 74 | * @returns {number} 75 | */ 76 | export function eventClientX(event) { 77 | return event.type === 'wheel' || 78 | event.type === 'pointerup' || 79 | event.type === 'pointerdown' || 80 | event.type === 'pointermove' || 81 | event.type === 'mousedown' || 82 | event.type === 'mousemove' || 83 | event.type === 'mouseup' ? event.clientX : event.changedTouches[0].clientX; 84 | } 85 | 86 | /** 87 | * @param {Event} event 88 | * @returns {number} 89 | */ 90 | export function eventClientY(event) { 91 | return event.type === 'wheel' || 92 | event.type === 'pointerup' || 93 | event.type === 'pointerdown' || 94 | event.type === 'pointermove' || 95 | event.type === 'mousedown' || 96 | event.type === 'mousemove' || 97 | event.type === 'mouseup' ? event.clientY : event.changedTouches[0].clientY; 98 | } 99 | 100 | /** 101 | * @param {HTMLElement} $element 102 | * @param {number} left 103 | * @param {number} top 104 | * @param {number} scale 105 | */ 106 | export function transform($element, left, top, scale) { 107 | $element.style.transform = `translate(${ left }px, ${ top }px) scale(${ scale })`; 108 | } 109 | 110 | /** 111 | * @param {HTMLElement} $element 112 | * @param {number} time 113 | */ 114 | export function transition($element, time) { 115 | if (time) { 116 | $element.style.transition = `transform ${ time }s`; 117 | } else { 118 | $element.style.removeProperty('transition'); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/wheel-zoom.js: -------------------------------------------------------------------------------- 1 | import { 2 | getElementPosition, 3 | getPageScrollLeft, 4 | getPageScrollTop, 5 | on, 6 | off, 7 | eventClientX, 8 | eventClientY, 9 | isTouch, 10 | transition, 11 | transform, 12 | } from './toolkit'; 13 | import { 14 | calculateAlignPoint, 15 | calculateContentMaxShift, 16 | calculateContentShift, 17 | calculateCorrectPoint, 18 | calculateViewportCenter, 19 | } from './calculator'; 20 | import { wZoomDefaultOptions } from './default-options.js'; 21 | import DragScrollableObserver, { EVENT_DROP, EVENT_GRAB, EVENT_MOVE } from './observers/DragScrollableObserver'; 22 | import InteractionObserver, { EVENT_CLICK, EVENT_DBLCLICK, EVENT_WHEEL } from './observers/InteractionObserver'; 23 | import PinchToZoomObserver, { EVENT_PINCH_TO_ZOOM } from './observers/PinchToZoomObserver'; 24 | 25 | /** 26 | * @class WZoom 27 | * @param {string|HTMLElement} selectorOrHTMLElement 28 | * @param {WZoomOptions} options 29 | * @constructor 30 | */ 31 | function WZoom(selectorOrHTMLElement, options = {}) { 32 | this._init = this._init.bind(this); 33 | this._prepare = this._prepare.bind(this); 34 | this._computeScale = this._computeScale.bind(this); 35 | this._computePosition = this._computePosition.bind(this); 36 | this._transform = this._transform.bind(this); 37 | 38 | /** @type {WZoomContent} */ 39 | this.content = {}; 40 | 41 | if (typeof selectorOrHTMLElement === 'string') { 42 | this.content.$element = document.querySelector(selectorOrHTMLElement); 43 | 44 | if (!this.content.$element) { 45 | throw `WZoom: Element with selector \`${ selectorOrHTMLElement }\` not found`; 46 | } 47 | } else if (selectorOrHTMLElement instanceof HTMLElement) { 48 | this.content.$element = selectorOrHTMLElement; 49 | } else { 50 | throw `WZoom: \`selectorOrHTMLElement\` must be selector or HTMLElement, and not ${ {}.toString.call(selectorOrHTMLElement) }`; 51 | } 52 | 53 | /** @type {WZoomViewport} */ 54 | this.viewport = {}; 55 | // for viewport take just the parent 56 | this.viewport.$element = this.content.$element.parentElement; 57 | 58 | /** @type {WZoomOptions} */ 59 | this.options = optionsConstructor(options, wZoomDefaultOptions); 60 | 61 | // check if we're using a touch screen 62 | this.isTouch = isTouch(); 63 | this.direction = 1; 64 | /** @type {AbstractObserver[]} */ 65 | this.observers = []; 66 | 67 | if (this.options.type === 'image') { 68 | // if the `image` has already been loaded 69 | if (this.content.$element.complete) { 70 | this._init(); 71 | } else { 72 | on(this.content.$element, 'load', this._init, { once: true }); 73 | } 74 | } else { 75 | this._init(); 76 | } 77 | } 78 | 79 | WZoom.prototype = { 80 | constructor: WZoom, 81 | /** 82 | * @private 83 | */ 84 | _init() { 85 | const { viewport, content, options, observers } = this; 86 | 87 | this._prepare(); 88 | this._destroyObservers(); 89 | 90 | if (options.dragScrollable === true) { 91 | const dragScrollableObserver = new DragScrollableObserver(content.$element); 92 | observers.push(dragScrollableObserver); 93 | 94 | if (typeof options.onGrab === 'function') { 95 | dragScrollableObserver.on(EVENT_GRAB, (event) => { 96 | event.preventDefault(); 97 | 98 | options.onGrab(event, this); 99 | }); 100 | } 101 | 102 | if (typeof options.onDrop === 'function') { 103 | dragScrollableObserver.on(EVENT_DROP, (event) => { 104 | event.preventDefault(); 105 | 106 | options.onDrop(event, this); 107 | }); 108 | } 109 | 110 | dragScrollableObserver.on(EVENT_MOVE, (event) => { 111 | event.preventDefault(); 112 | 113 | const { x, y } = event.data; 114 | 115 | const contentNewLeft = content.currentLeft + x; 116 | const contentNewTop = content.currentTop + y; 117 | 118 | let maxAvailableLeft = (content.currentWidth - viewport.originalWidth) / 2 + content.correctX; 119 | let maxAvailableTop = (content.currentHeight - viewport.originalHeight) / 2 + content.correctY; 120 | 121 | // if we do not go beyond the permissible boundaries of the viewport 122 | if (Math.abs(contentNewLeft) <= maxAvailableLeft) content.currentLeft = contentNewLeft; 123 | // if we do not go beyond the permissible boundaries of the viewport 124 | if (Math.abs(contentNewTop) <= maxAvailableTop) content.currentTop = contentNewTop; 125 | 126 | this._transform(options.smoothTimeDrag); 127 | 128 | if (typeof options.onMove === 'function') { 129 | options.onMove(event, this); 130 | } 131 | }); 132 | } 133 | 134 | const interactionObserver = new InteractionObserver(content.$element); 135 | observers.push(interactionObserver); 136 | 137 | if (!options.disableWheelZoom) { 138 | if (this.isTouch) { 139 | const pinchToZoomObserver = new PinchToZoomObserver(content.$element); 140 | observers.push(pinchToZoomObserver); 141 | 142 | pinchToZoomObserver.on(EVENT_PINCH_TO_ZOOM, (event) => { 143 | const { clientX, clientY, direction } = event.data; 144 | 145 | const scale = this._computeScale(direction); 146 | this._computePosition(scale, clientX, clientY); 147 | this._transform(); 148 | }); 149 | } else { 150 | interactionObserver.on(EVENT_WHEEL, (event) => { 151 | event.preventDefault(); 152 | 153 | const direction = options.reverseWheelDirection ? -event.deltaY : event.deltaY; 154 | const scale = this._computeScale(direction); 155 | this._computePosition(scale, eventClientX(event), eventClientY(event)); 156 | this._transform(); 157 | }); 158 | } 159 | } 160 | 161 | if (options.zoomOnClick || options.zoomOnDblClick) { 162 | const eventType = options.zoomOnDblClick ? EVENT_DBLCLICK : EVENT_CLICK; 163 | 164 | interactionObserver.on(eventType, (event) => { 165 | const scale = this.direction === 1 ? content.maxScale : content.minScale; 166 | this._computePosition(scale, eventClientX(event), eventClientY(event)); 167 | this._transform(); 168 | 169 | this.direction *= -1; 170 | }); 171 | } 172 | }, 173 | /** 174 | * @private 175 | */ 176 | _prepare() { 177 | const { viewport, content, options } = this; 178 | const { left, top } = getElementPosition(viewport.$element); 179 | 180 | viewport.originalWidth = viewport.$element.offsetWidth; 181 | viewport.originalHeight = viewport.$element.offsetHeight; 182 | viewport.originalLeft = left; 183 | viewport.originalTop = top; 184 | 185 | if (options.type === 'image') { 186 | content.originalWidth = options.width || content.$element.naturalWidth; 187 | content.originalHeight = options.height || content.$element.naturalHeight; 188 | } else { 189 | content.originalWidth = options.width || content.$element.offsetWidth; 190 | content.originalHeight = options.height || content.$element.offsetHeight; 191 | } 192 | 193 | content.maxScale = options.maxScale; 194 | content.minScale = options.minScale || Math.min(viewport.originalWidth / content.originalWidth, viewport.originalHeight / content.originalHeight, content.maxScale); 195 | 196 | content.currentScale = content.minScale; 197 | content.currentWidth = content.originalWidth * content.currentScale; 198 | content.currentHeight = content.originalHeight * content.currentScale; 199 | 200 | [ content.alignPointX, content.alignPointY ] = calculateAlignPoint(viewport, content, options.alignContent); 201 | 202 | content.currentLeft = content.alignPointX; 203 | content.currentTop = content.alignPointY; 204 | 205 | // calculate indent-left and indent-top to of content from viewport borders 206 | [ content.correctX, content.correctY ] = calculateCorrectPoint(viewport, content, options.alignContent); 207 | 208 | if (typeof options.prepare === 'function') { 209 | options.prepare(this); 210 | } 211 | 212 | this._transform(); 213 | }, 214 | /** 215 | * @private 216 | */ 217 | _computeScale(direction) { 218 | this.direction = direction < 0 ? 1 : -1; 219 | 220 | const { minScale, maxScale, currentScale } = this.content; 221 | 222 | const scale = currentScale * Math.pow(this.options.speed, this.direction); 223 | 224 | if (scale <= minScale) { 225 | this.direction = 1; 226 | return minScale; 227 | } 228 | 229 | if (scale >= maxScale) { 230 | this.direction = -1; 231 | return maxScale; 232 | } 233 | 234 | return scale; 235 | }, 236 | /** 237 | * @param {number} scale 238 | * @param {number} x 239 | * @param {number} y 240 | * @private 241 | */ 242 | _computePosition(scale, x, y) { 243 | const { viewport, content, options, direction } = this; 244 | 245 | const contentNewWidth = content.originalWidth * scale; 246 | const contentNewHeight = content.originalHeight * scale; 247 | 248 | const scrollLeft = getPageScrollLeft(); 249 | const scrollTop = getPageScrollTop(); 250 | 251 | // calculate the parameters along the X axis 252 | let contentNewLeft = calculateContentShift(x, scrollLeft, viewport.originalLeft, content.currentLeft, viewport.originalWidth, contentNewWidth / content.currentWidth); 253 | // calculate the parameters along the Y axis 254 | let contentNewTop = calculateContentShift(y, scrollTop, viewport.originalTop, content.currentTop, viewport.originalHeight, contentNewHeight / content.currentHeight); 255 | 256 | if (direction === -1) { 257 | // check that the content does not go beyond the X axis 258 | contentNewLeft = calculateContentMaxShift(options.alignContent, viewport.originalWidth, content.correctX, contentNewWidth, contentNewLeft); 259 | // check that the content does not go beyond the Y axis 260 | contentNewTop = calculateContentMaxShift(options.alignContent, viewport.originalHeight, content.correctY, contentNewHeight, contentNewTop); 261 | } 262 | 263 | if (scale === content.minScale) { 264 | contentNewLeft = content.alignPointX; 265 | contentNewTop = content.alignPointY; 266 | } 267 | 268 | content.currentWidth = contentNewWidth; 269 | content.currentHeight = contentNewHeight; 270 | content.currentLeft = contentNewLeft; 271 | content.currentTop = contentNewTop; 272 | content.currentScale = scale; 273 | }, 274 | /** 275 | * @param {number} smoothTime 276 | * @private 277 | */ 278 | _transform(smoothTime) { 279 | if (smoothTime === undefined) smoothTime = this.options.smoothTime; 280 | 281 | transition(this.content.$element, smoothTime); 282 | transform(this.content.$element, this.content.currentLeft, this.content.currentTop, this.content.currentScale); 283 | 284 | if (typeof this.options.rescale === 'function') { 285 | this.options.rescale(this); 286 | } 287 | }, 288 | /** 289 | * todo добавить проверку на то что бы переданные координаты не выходили за пределы возможного 290 | * @param {number} scale 291 | * @param {Object} coordinates 292 | * @private 293 | */ 294 | _zoom(scale, coordinates = {}) { 295 | // if the coordinates are not passed, then use the coordinates of the center 296 | if (coordinates.x === undefined || coordinates.y === undefined) { 297 | coordinates = calculateViewportCenter(this.viewport); 298 | } 299 | 300 | this._computePosition(scale, coordinates.x, coordinates.y); 301 | this._transform(); 302 | }, 303 | _destroyObservers() { 304 | for (const observer of this.observers) { 305 | observer.destroy(); 306 | } 307 | }, 308 | prepare() { 309 | this._prepare(); 310 | }, 311 | /** 312 | * todo добавить проверку на то что бы переданный state вообще возможен для данного instance 313 | * @param {number} top 314 | * @param {number} left 315 | * @param {number} scale 316 | */ 317 | transform(top, left, scale) { 318 | const { content } = this; 319 | 320 | content.currentWidth = content.originalWidth * scale; 321 | content.currentHeight = content.originalHeight * scale; 322 | content.currentLeft = left; 323 | content.currentTop = top; 324 | content.currentScale = scale; 325 | 326 | this._transform(); 327 | }, 328 | zoomUp() { 329 | this._zoom(this._computeScale(-1)); 330 | }, 331 | zoomDown() { 332 | this._zoom(this._computeScale(1)); 333 | }, 334 | maxZoomUp() { 335 | this._zoom(this.content.maxScale); 336 | }, 337 | maxZoomDown() { 338 | this._zoom(this.content.minScale); 339 | }, 340 | zoomUpToPoint(coordinates) { 341 | this._zoom(this._computeScale(-1), coordinates); 342 | }, 343 | zoomDownToPoint(coordinates) { 344 | this._zoom(this._computeScale(1), coordinates); 345 | }, 346 | maxZoomUpToPoint(coordinates) { 347 | this._zoom(this.content.maxScale, coordinates); 348 | }, 349 | destroy() { 350 | this.content.$element.style.removeProperty('transition'); 351 | this.content.$element.style.removeProperty('transform'); 352 | 353 | if (this.options.type === 'image') { 354 | off(this.content.$element, 'load', this._init); 355 | } 356 | 357 | this._destroyObservers(); 358 | 359 | for (let key in this) { 360 | if (this.hasOwnProperty(key)) { 361 | this[key] = null; 362 | } 363 | } 364 | } 365 | }; 366 | 367 | /** 368 | * @param {?WZoomOptions} targetOptions 369 | * @param {?WZoomOptions} defaultOptions 370 | * @returns {?WZoomOptions} 371 | */ 372 | function optionsConstructor(targetOptions, defaultOptions) { 373 | const options = Object.assign({}, defaultOptions, targetOptions); 374 | 375 | if (isTouch()) { 376 | options.smoothTime = 0; 377 | options.smoothTimeDrag = 0; 378 | } else { 379 | const smoothTime = Number(options.smoothTime); 380 | const smoothTimeDrag = Number(options.smoothTimeDrag); 381 | 382 | options.smoothTime = !isNaN(smoothTime) ? smoothTime : wZoomDefaultOptions.smoothTime; 383 | options.smoothTimeDrag = !isNaN(smoothTimeDrag) ? smoothTimeDrag : options.smoothTime; 384 | } 385 | 386 | return options; 387 | } 388 | 389 | /** 390 | * Create WZoom instance 391 | * @param {string|HTMLElement} selectorOrHTMLElement 392 | * @param {WZoomOptions} [options] 393 | * @returns {WZoom} 394 | */ 395 | WZoom.create = function (selectorOrHTMLElement, options = {}) { 396 | return new WZoom(selectorOrHTMLElement, options); 397 | }; 398 | 399 | export default WZoom; 400 | 401 | /** 402 | * @typedef WZoomContent 403 | * @type {Object} 404 | * @property {HTMLElement} [$element] 405 | * @property {number} [originalWidth] 406 | * @property {number} [originalHeight] 407 | * @property {number} [currentWidth] 408 | * @property {number} [currentHeight] 409 | * @property {number} [currentLeft] 410 | * @property {number} [currentTop] 411 | * @property {number} [currentScale] 412 | * @property {number} [maxScale] 413 | * @property {number} [minScale] 414 | * @property {number} [alignPointX] 415 | * @property {number} [alignPointY] 416 | * @property {number} [correctX] 417 | * @property {number} [correctY] 418 | */ 419 | 420 | /** 421 | * @typedef WZoomViewport 422 | * @type {Object} 423 | * @property {HTMLElement} [$element] 424 | * @property {number} [originalWidth] 425 | * @property {number} [originalHeight] 426 | * @property {number} [originalLeft] 427 | * @property {number} [originalTop] 428 | */ 429 | --------------------------------------------------------------------------------