├── .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 | 
6 | [](https://github.com/worka/vanilla-js-wheel-zoom/stargazers)
7 | [](https://github.com/worka/vanilla-js-wheel-zoom/issues)
8 | [](https://github.com/worka/vanilla-js-wheel-zoom/network)
9 | [](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 |
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 |
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 | Zoom Up
117 | Zoom Down
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 | Zoom Down
49 |
50 | Zoom Up
51 |
52 |
53 |
54 |
55 |
56 |
Bridge
57 |
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 |
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 | Max Down
39 | Zoom Down
40 |
41 |
42 |
43 | Zoom Up
44 | Max Up
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | Destroy
56 | Change image
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 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
Loading...
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
Loading...
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
Loading...
75 |
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 |
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 |
--------------------------------------------------------------------------------