@hyper-proto/iv-viewer 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,988 @@
1
+ import {
2
+ createElement,
3
+ addClass,
4
+ removeClass,
5
+ css,
6
+ removeCss,
7
+ wrap,
8
+ unwrap,
9
+ remove,
10
+ easeOutQuart,
11
+ imageLoaded,
12
+ clamp,
13
+ assignEvent,
14
+ getTouchPointsDistance,
15
+ preventDefault,
16
+ ZOOM_CONSTANT,
17
+ MOUSE_WHEEL_COUNT,
18
+ } from './util';
19
+
20
+ import Slider from './Slider';
21
+ class ImageViewer {
22
+ get zoomInButton () {
23
+ return this._options.hasZoomButtons ? '<div class="iv-button-zoom--in" role="button"></div>' : '';
24
+ }
25
+
26
+ get zoomOutButton () {
27
+ return this._options.hasZoomButtons ? '<div class="iv-button-zoom--out" role="button"></div>' : '';
28
+ }
29
+
30
+ get imageViewHtml () {
31
+ return `
32
+ <div class="iv-loader"></div>
33
+ <div class="iv-snap-view">
34
+ <div class="iv-snap-image-wrap">
35
+ <div class="iv-snap-handle"></div>
36
+ </div>
37
+ <div class="iv-zoom-actions ${this._options.hasZoomButtons ? 'iv-zoom-actions--has-buttons' : ''}">
38
+ ${this.zoomInButton}
39
+ <div class="iv-zoom-slider">
40
+ <div class="iv-zoom-handle"></div>
41
+ </div>
42
+ ${this.zoomOutButton}
43
+ </div>
44
+ </div>
45
+ <div class="iv-image-view" >
46
+ <div class="iv-image-wrap" ></div>
47
+ </div>
48
+ `;
49
+ }
50
+
51
+ constructor (element, options = {}) {
52
+ const { container, domElement, imageSrc, hiResImageSrc } = this._findContainerAndImageSrc(element, options);
53
+
54
+ // containers for elements
55
+ this._elements = {
56
+ container,
57
+ domElement,
58
+ };
59
+
60
+ this._options = { ...ImageViewer.defaults, ...options };
61
+
62
+ // container for all events
63
+ this._events = {
64
+
65
+ };
66
+
67
+ this._listeners = this._options.listeners || {};
68
+
69
+ // container for all timeout and frames
70
+ this._frames = {
71
+
72
+ };
73
+
74
+ // container for all sliders
75
+ this._sliders = {
76
+
77
+ };
78
+
79
+ // maintain current state
80
+ this._state = {
81
+ zoomValue: this._options.zoomValue,
82
+ };
83
+
84
+ this._images = {
85
+ imageSrc,
86
+ hiResImageSrc,
87
+ };
88
+
89
+ this._init();
90
+
91
+ if (imageSrc) {
92
+ this._loadImages();
93
+ }
94
+
95
+ // store reference of imageViewer in domElement
96
+ domElement._imageViewer = this;
97
+ }
98
+
99
+ _findContainerAndImageSrc (element) {
100
+ let domElement = element;
101
+ let imageSrc, hiResImageSrc;
102
+
103
+ if (typeof element === 'string') {
104
+ domElement = document.querySelector(element);
105
+ }
106
+
107
+ // throw error if imageViewer is already assigned
108
+ if (domElement._imageViewer) {
109
+ throw new Error('An image viewer is already being initiated on the element.');
110
+ }
111
+
112
+ let container = element;
113
+
114
+ if (domElement.tagName === 'IMG') {
115
+ imageSrc = domElement.src;
116
+ hiResImageSrc = domElement.getAttribute('high-res-src') || domElement.getAttribute('data-high-res-src');
117
+
118
+ // wrap the image with iv-container div
119
+ container = wrap(domElement, { className: 'iv-container iv-image-mode', style: { display: 'inline-block', overflow: 'hidden' } });
120
+
121
+ // hide the image and add iv-original-img class
122
+ css(domElement, {
123
+ opacity: 0,
124
+ position: 'relative',
125
+ zIndex: -1,
126
+ });
127
+ } else {
128
+ imageSrc = domElement.getAttribute('src') || domElement.getAttribute('data-src');
129
+ hiResImageSrc = domElement.getAttribute('high-res-src') || domElement.getAttribute('data-high-res-src');
130
+ }
131
+
132
+ return {
133
+ container,
134
+ domElement,
135
+ imageSrc,
136
+ hiResImageSrc,
137
+ };
138
+ }
139
+
140
+ _init () {
141
+ // initialize the dom elements
142
+ this._initDom();
143
+
144
+ // initialize slider
145
+ this._initImageSlider();
146
+ this._initSnapSlider();
147
+ this._initZoomSlider();
148
+
149
+ // enable pinch and zoom feature for touch screens
150
+ this._pinchAndZoom();
151
+
152
+ // enable scroll zoom interaction
153
+ this._scrollZoom();
154
+
155
+ // enable double tap to zoom interaction
156
+ this._doubleTapToZoom();
157
+
158
+ // initialize events
159
+ this._initEvents();
160
+ }
161
+
162
+ _initDom () {
163
+ const { container } = this._elements;
164
+
165
+ // add image-viewer layout elements
166
+ createElement({
167
+ tagName: 'div',
168
+ className: 'iv-wrap',
169
+ html: this.imageViewHtml,
170
+ parent: container,
171
+ });
172
+
173
+ // add container class on the container
174
+ addClass(container, 'iv-container');
175
+
176
+ // if the element is static position, position it relatively
177
+ if (css(container, 'position') === 'static') {
178
+ css(container, { position: 'relative' });
179
+ }
180
+
181
+ // save references for later use
182
+ this._elements = {
183
+ ...this._elements,
184
+ snapView: container.querySelector('.iv-snap-view'),
185
+ snapImageWrap: container.querySelector('.iv-snap-image-wrap'),
186
+ imageWrap: container.querySelector('.iv-image-wrap'),
187
+ snapHandle: container.querySelector('.iv-snap-handle'),
188
+ zoomHandle: container.querySelector('.iv-zoom-handle'),
189
+ zoomIn: container.querySelector('.iv-button-zoom--in'),
190
+ zoomOut: container.querySelector('.iv-button-zoom--out'),
191
+ };
192
+
193
+ if (this._listeners.onInit) {
194
+ this._listeners.onInit(this._callbackData);
195
+ }
196
+ }
197
+
198
+ _initImageSlider () {
199
+ const {
200
+ _elements,
201
+ } = this;
202
+
203
+ const { imageWrap } = _elements;
204
+
205
+ let positions, currentPos;
206
+
207
+ /* Add slide interaction to image */
208
+ const imageSlider = new Slider(imageWrap, {
209
+ isSliderEnabled: () => {
210
+ const { loaded, zooming, zoomValue } = this._state;
211
+ return loaded && !zooming && zoomValue > 100;
212
+ },
213
+ onStart: (e, position) => {
214
+ const { snapSlider } = this._sliders;
215
+
216
+ // clear all animation frame and interval
217
+ this._clearFrames();
218
+
219
+ snapSlider.onStart();
220
+
221
+ // reset positions
222
+ positions = [position, position];
223
+ currentPos = undefined;
224
+
225
+ this._frames.slideMomentumCheck = setInterval(() => {
226
+ if (!currentPos) return;
227
+
228
+ positions.shift();
229
+ positions.push({
230
+ x: currentPos.mx,
231
+ y: currentPos.my,
232
+ });
233
+ }, 50);
234
+ },
235
+ onMove: (e, position) => {
236
+ const { snapImageDim } = this._state;
237
+ const { snapSlider } = this._sliders;
238
+ const imageCurrentDim = this._getImageCurrentDim();
239
+ currentPos = position;
240
+
241
+ snapSlider.onMove(e, {
242
+ dx: -position.dx * snapImageDim.w / imageCurrentDim.w,
243
+ dy: -position.dy * snapImageDim.h / imageCurrentDim.h,
244
+ });
245
+ },
246
+ onEnd: () => {
247
+ const { snapImageDim } = this._state;
248
+ const { snapSlider } = this._sliders;
249
+ const imageCurrentDim = this._getImageCurrentDim();
250
+
251
+ // clear all animation frame and interval
252
+ this._clearFrames();
253
+
254
+ let step, positionX, positionY;
255
+
256
+ const xDiff = positions[1].x - positions[0].x;
257
+ const yDiff = positions[1].y - positions[0].y;
258
+
259
+ const momentum = () => {
260
+ if (step <= 60) {
261
+ this._frames.sliderMomentumFrame = requestAnimationFrame(momentum);
262
+ }
263
+
264
+ positionX += easeOutQuart(step, xDiff / 3, -xDiff / 3, 60);
265
+ positionY += easeOutQuart(step, yDiff / 3, -yDiff / 3, 60);
266
+
267
+ snapSlider.onMove(null, {
268
+ dx: -(positionX * snapImageDim.w / imageCurrentDim.w),
269
+ dy: -(positionY * snapImageDim.h / imageCurrentDim.h),
270
+ });
271
+
272
+ step++;
273
+ };
274
+
275
+ if (Math.abs(xDiff) > 30 || Math.abs(yDiff) > 30) {
276
+ step = 1;
277
+ positionX = currentPos.dx;
278
+ positionY = currentPos.dy;
279
+
280
+ momentum();
281
+ }
282
+ },
283
+ });
284
+
285
+ imageSlider.init();
286
+
287
+ this._sliders.imageSlider = imageSlider;
288
+ }
289
+
290
+ _initSnapSlider () {
291
+ const {
292
+ snapHandle,
293
+ } = this._elements;
294
+
295
+ let startHandleTop, startHandleLeft;
296
+
297
+ const snapSlider = new Slider(snapHandle, {
298
+ isSliderEnabled: () => this._state.loaded,
299
+ onStart: () => {
300
+ const { slideMomentumCheck, sliderMomentumFrame } = this._frames;
301
+
302
+ startHandleTop = parseFloat(css(snapHandle, 'top'));
303
+ startHandleLeft = parseFloat(css(snapHandle, 'left'));
304
+
305
+ // stop momentum on image
306
+ clearInterval(slideMomentumCheck);
307
+ cancelAnimationFrame(sliderMomentumFrame);
308
+ },
309
+ onMove: (e, position) => {
310
+ const { snapHandleDim, snapImageDim } = this._state;
311
+ const { image } = this._elements;
312
+
313
+ const imageCurrentDim = this._getImageCurrentDim();
314
+
315
+ // find handle left and top and make sure they lay between the snap image
316
+ const maxLeft = Math.max(snapImageDim.w - snapHandleDim.w, startHandleLeft);
317
+ const maxTop = Math.max(snapImageDim.h - snapHandleDim.h, startHandleTop);
318
+ const minLeft = Math.min(0, startHandleLeft);
319
+ const minTop = Math.min(0, startHandleTop);
320
+
321
+ const left = clamp(startHandleLeft + position.dx, minLeft, maxLeft);
322
+ const top = clamp(startHandleTop + position.dy, minTop, maxTop);
323
+
324
+ const imgLeft = -left * imageCurrentDim.w / snapImageDim.w;
325
+ const imgTop = -top * imageCurrentDim.h / snapImageDim.h;
326
+
327
+ css(snapHandle, {
328
+ left: `${left}px`,
329
+ top: `${top}px`,
330
+ });
331
+
332
+ css(image, {
333
+ left: `${imgLeft}px`,
334
+ top: `${imgTop}px`,
335
+ });
336
+ },
337
+ });
338
+
339
+ snapSlider.init();
340
+
341
+ this._sliders.snapSlider = snapSlider;
342
+ }
343
+
344
+ _initZoomSlider () {
345
+ const { snapView, zoomHandle } = this._elements;
346
+
347
+ // zoom in zoom out using zoom handle
348
+ const sliderElm = snapView.querySelector('.iv-zoom-slider');
349
+
350
+ let leftOffset, handleWidth;
351
+
352
+ // on zoom slider we have to follow the mouse and set the handle to its position.
353
+ const zoomSlider = new Slider(sliderElm, {
354
+ isSliderEnabled: () => this._state.loaded,
355
+ onStart: (eStart) => {
356
+ const { zoomSlider: slider } = this._sliders;
357
+
358
+ leftOffset = sliderElm.getBoundingClientRect().left;
359
+ handleWidth = parseInt(css(zoomHandle, 'width'), 10);
360
+
361
+ // move the handle to current mouse position
362
+ slider.onMove(eStart);
363
+ },
364
+ onMove: (e) => {
365
+ const { maxZoom } = this._options;
366
+ const { zoomSliderLength } = this._state;
367
+
368
+ const clientX = e.clientX !== undefined ? e.clientX : e.touches[0].clientX;
369
+
370
+ const newLeft = clamp(clientX - leftOffset - handleWidth / 2, 0, zoomSliderLength);
371
+
372
+ const zoomValue = 100 + ((maxZoom - 100) * newLeft / zoomSliderLength);
373
+
374
+ this.zoom(zoomValue);
375
+ },
376
+ });
377
+
378
+ zoomSlider.init();
379
+ this._sliders.zoomSlider = zoomSlider;
380
+ }
381
+
382
+ _initEvents () {
383
+ this._snapViewEvents();
384
+
385
+ // handle window resize
386
+ if (this._options.refreshOnResize) {
387
+ this._events.onWindowResize = assignEvent(window, 'resize', this.refresh);
388
+ }
389
+ this._events.onDragStart = assignEvent(this._elements.container, 'dragstart', preventDefault);
390
+ }
391
+
392
+ _snapViewEvents () {
393
+ const { imageWrap, snapView } = this._elements;
394
+
395
+ // show snapView on mouse move
396
+ this._events.snapViewOnMouseMove = assignEvent(imageWrap, ['touchmove', 'mousemove'], () => {
397
+ this.showSnapView();
398
+ });
399
+
400
+ // keep showing snapView if on hover over it without any timeout
401
+ this._events.mouseEnterSnapView = assignEvent(snapView, ['mouseenter', 'touchstart'], () => {
402
+ this._state.snapViewVisible = false;
403
+ this.showSnapView(true);
404
+ });
405
+
406
+ // on mouse leave set timeout to hide snapView
407
+ this._events.mouseLeaveSnapView = assignEvent(snapView, ['mouseleave', 'touchend'], () => {
408
+ this._state.snapViewVisible = false;
409
+ this.showSnapView();
410
+ });
411
+
412
+ if (!this._options.hasZoomButtons) {
413
+ return;
414
+ }
415
+ const { zoomOut, zoomIn } = this._elements;
416
+ this._events.zoomInClick = assignEvent(zoomIn, ['click'], () => {
417
+ this.zoom(this._state.zoomValue + this._options.zoomStep || 50);
418
+ });
419
+
420
+ this._events.zoomOutClick = assignEvent(zoomOut, ['click'], () => {
421
+ this.zoom(this._state.zoomValue - this._options.zoomStep || 50);
422
+ });
423
+ }
424
+
425
+ _pinchAndZoom () {
426
+ const { imageWrap, container } = this._elements;
427
+
428
+ // apply pinch and zoom feature
429
+ const onPinchStart = (eStart) => {
430
+ const { loaded, zoomValue: startZoomValue } = this._state;
431
+ const { _events: events } = this;
432
+
433
+ if (!loaded) return;
434
+
435
+ const touch0 = eStart.touches[0];
436
+ const touch1 = eStart.touches[1];
437
+
438
+ if (!(touch0 && touch1)) {
439
+ return;
440
+ }
441
+
442
+ this._state.zooming = true;
443
+
444
+ const contOffset = container.getBoundingClientRect();
445
+
446
+ // find distance between two touch points
447
+ const startDist = getTouchPointsDistance(eStart.touches);
448
+
449
+ // find the center for the zoom
450
+ const center = {
451
+ x: (touch1.clientX + touch0.clientX) / 2 - contOffset.left,
452
+ y: (touch1.clientY + touch0.clientY) / 2 - contOffset.top,
453
+ };
454
+
455
+ const moveListener = (eMove) => {
456
+ // eMove.preventDefault();
457
+
458
+ const newDist = getTouchPointsDistance(eMove.touches);
459
+
460
+ const zoomValue = startZoomValue + (newDist - startDist) / 2;
461
+
462
+ this.zoom(zoomValue, center);
463
+ };
464
+
465
+ const endListener = (eEnd) => {
466
+ // unbind events
467
+ events.pinchMove();
468
+ events.pinchEnd();
469
+ this._state.zooming = false;
470
+ // properly resume move event if one finger remains
471
+ if (eEnd.touches.length === 1) {
472
+ this._sliders.imageSlider.startHandler(eEnd);
473
+ }
474
+ };
475
+
476
+ // remove events if already assigned
477
+ if (events.pinchMove) events.pinchMove();
478
+ if (events.pinchEnd) events.pinchEnd();
479
+
480
+ // assign events
481
+ events.pinchMove = assignEvent(document, 'touchmove', moveListener);
482
+ events.pinchEnd = assignEvent(document, 'touchend', endListener);
483
+ };
484
+
485
+ this._events.pinchStart = assignEvent(imageWrap, 'touchstart', onPinchStart);
486
+ }
487
+
488
+ _scrollZoom () {
489
+ /* Add zoom interaction in mouse wheel */
490
+ const { _options } = this;
491
+ const { container, imageWrap } = this._elements;
492
+
493
+ let changedDelta = 0;
494
+
495
+ const onMouseWheel = (e) => {
496
+ const { loaded, zoomValue } = this._state;
497
+
498
+ if (!_options.zoomOnMouseWheel || !loaded) return;
499
+
500
+ // clear all animation frame and interval
501
+ this._clearFrames();
502
+
503
+ // cross-browser wheel delta
504
+ const delta = Math.max(-1, Math.min(1, e.wheelDelta || -e.detail || -e.deltaY));
505
+
506
+ const newZoomValue = zoomValue * (100 + delta * ZOOM_CONSTANT) / 100;
507
+
508
+ if (!(newZoomValue >= 100 && newZoomValue <= _options.maxZoom)) {
509
+ changedDelta += Math.abs(delta);
510
+ } else {
511
+ changedDelta = 0;
512
+ }
513
+
514
+ e.preventDefault();
515
+
516
+ if (changedDelta > MOUSE_WHEEL_COUNT) return;
517
+
518
+ const contOffset = container.getBoundingClientRect();
519
+
520
+ const x = e.clientX - contOffset.left;
521
+ const y = e.clientY - contOffset.top;
522
+
523
+ this.zoom(newZoomValue, {
524
+ x,
525
+ y,
526
+ });
527
+
528
+ // show the snap viewer
529
+ this.showSnapView();
530
+ };
531
+
532
+ this._events.scrollZoom = assignEvent(imageWrap, 'wheel', onMouseWheel);
533
+ }
534
+
535
+ _doubleTapToZoom () {
536
+ const { imageWrap } = this._elements;
537
+ // handle double tap for zoom in and zoom out
538
+
539
+ let touchTime = 0;
540
+
541
+ let point;
542
+
543
+ const onDoubleTap = (e) => {
544
+ if (touchTime === 0) {
545
+ touchTime = Date.now();
546
+ point = {
547
+ x: e.clientX,
548
+ y: e.clientY,
549
+ };
550
+ } else if (Date.now() - touchTime < 500 && Math.abs(e.clientX - point.x) < 50 && Math.abs(e.clientY - point.y) < 50) {
551
+ if (this._state.zoomValue === this._options.zoomValue) {
552
+ this.zoom(200);
553
+ } else {
554
+ this.resetZoom();
555
+ }
556
+ touchTime = 0;
557
+ } else {
558
+ touchTime = 0;
559
+ }
560
+ };
561
+
562
+ this._events.doubleTapToZoom = assignEvent(imageWrap, 'click', onDoubleTap);
563
+ }
564
+
565
+ _getImageCurrentDim () {
566
+ const { zoomValue, imageDim } = this._state;
567
+ return {
568
+ w: imageDim.w * (zoomValue / 100),
569
+ h: imageDim.h * (zoomValue / 100),
570
+ };
571
+ }
572
+
573
+ _loadImages () {
574
+ const { _images, _elements } = this;
575
+ const { imageSrc, hiResImageSrc } = _images;
576
+ const { container, snapImageWrap, imageWrap } = _elements;
577
+
578
+ const ivLoader = container.querySelector('.iv-loader');
579
+
580
+ // remove old images
581
+ remove(container.querySelectorAll('.iv-snap-image, .iv-image'));
582
+
583
+ // add snapView image
584
+ const snapImage = createElement({
585
+ tagName: 'img',
586
+ className: 'iv-snap-image',
587
+ src: imageSrc,
588
+ insertBefore: snapImageWrap.firstChild,
589
+ parent: snapImageWrap,
590
+ });
591
+
592
+ // add image
593
+ const image = createElement({
594
+ tagName: 'img',
595
+ className: 'iv-image iv-small-image',
596
+ src: imageSrc,
597
+ parent: imageWrap,
598
+ });
599
+
600
+ this._state.loaded = false;
601
+
602
+ // store image reference in _elements
603
+ this._elements.image = image;
604
+ this._elements.snapImage = snapImage;
605
+
606
+ css(ivLoader, { display: 'block' });
607
+
608
+ // keep visibility hidden until image is loaded
609
+ css(image, { visibility: 'hidden' });
610
+
611
+ // hide snap view if open
612
+ this.hideSnapView();
613
+
614
+ const onImageLoad = () => {
615
+ // hide the iv loader
616
+ css(ivLoader, { display: 'none' });
617
+
618
+ // show the image
619
+ css(image, { visibility: 'visible' });
620
+
621
+ // load high resolution image if provided
622
+ if (hiResImageSrc) {
623
+ this._loadHighResImage(hiResImageSrc);
624
+ }
625
+
626
+ // set loaded flag to true
627
+ this._state.loaded = true;
628
+
629
+ // calculate the dimension
630
+ this._calculateDimensions();
631
+
632
+ // dispatch image load event
633
+ if (this._listeners.onImageLoaded) {
634
+ this._listeners.onImageLoaded(this._callbackData);
635
+ }
636
+
637
+ // reset the zoom
638
+ this.resetZoom();
639
+ };
640
+
641
+ if (imageLoaded(image)) {
642
+ onImageLoad();
643
+ } else {
644
+ if (typeof this._events.imageLoad === 'function') {
645
+ this._events.imageLoad();
646
+ }
647
+ this._events.imageLoad = assignEvent(image, 'load', onImageLoad);
648
+ }
649
+ }
650
+
651
+ _loadHighResImage (hiResImageSrc) {
652
+ const { imageWrap, container } = this._elements;
653
+
654
+ const lowResImg = this._elements.image;
655
+
656
+ const hiResImage = createElement({
657
+ tagName: 'img',
658
+ className: 'iv-image iv-large-image',
659
+ src: hiResImageSrc,
660
+ parent: imageWrap,
661
+ style: lowResImg.style.cssText,
662
+ });
663
+
664
+ // add all the style attributes from lowResImg to highResImg
665
+ hiResImage.style.cssText = lowResImg.style.cssText;
666
+
667
+ this._elements.image = container.querySelectorAll('.iv-image');
668
+
669
+ const onHighResImageLoad = () => {
670
+ // remove the low size image and set this image as default image
671
+ remove(lowResImg);
672
+ this._elements.image = hiResImage;
673
+ // this._calculateDimensions();
674
+ };
675
+
676
+ if (imageLoaded(hiResImage)) {
677
+ onHighResImageLoad();
678
+ } else {
679
+ if (typeof this._events.hiResImageLoad === 'function') {
680
+ this._events.hiResImageLoad();
681
+ }
682
+ this._events.hiResImageLoad = assignEvent(hiResImage, 'load', onHighResImageLoad);
683
+ }
684
+ }
685
+
686
+ _calculateDimensions () {
687
+ const { image, container, snapView, snapImage, zoomHandle } = this._elements;
688
+
689
+ // calculate content width of image and snap image
690
+ const imageWidth = parseInt(css(image, 'width'), 10);
691
+ const imageHeight = parseInt(css(image, 'height'), 10);
692
+
693
+ const contWidth = parseInt(css(container, 'width'), 10);
694
+ const contHeight = parseInt(css(container, 'height'), 10);
695
+
696
+ const snapViewWidth = snapView.clientWidth;
697
+ const snapViewHeight = snapView.clientHeight;
698
+
699
+ // set the container dimension
700
+ this._state.containerDim = {
701
+ w: contWidth,
702
+ h: contHeight,
703
+ };
704
+
705
+ // set the image dimension
706
+ const ratio = imageWidth / imageHeight;
707
+
708
+ const imgWidth = (imageWidth > imageHeight && contHeight >= contWidth) || ratio * contHeight > contWidth
709
+ ? contWidth
710
+ : ratio * contHeight;
711
+
712
+ const imgHeight = imgWidth / ratio;
713
+
714
+ this._state.imageDim = {
715
+ w: imgWidth,
716
+ h: imgHeight,
717
+ };
718
+
719
+ // reset image position and zoom
720
+ css(image, {
721
+ width: `${imgWidth}px`,
722
+ height: `${imgHeight}px`,
723
+ left: `${(contWidth - imgWidth) / 2}px`,
724
+ top: `${(contHeight - imgHeight) / 2}px`,
725
+ maxWidth: 'none',
726
+ maxHeight: 'none',
727
+ });
728
+
729
+ // set the snap Image dimension
730
+ const snapWidth = imgWidth > imgHeight ? snapViewWidth : imgWidth * snapViewHeight / imgHeight;
731
+ const snapHeight = imgHeight > imgWidth ? snapViewHeight : imgHeight * snapViewWidth / imgWidth;
732
+
733
+ this._state.snapImageDim = {
734
+ w: snapWidth,
735
+ h: snapHeight,
736
+ };
737
+
738
+ css(snapImage, {
739
+ width: `${snapWidth}px`,
740
+ height: `${snapHeight}px`,
741
+ });
742
+
743
+ const zoomSlider = snapView.querySelector('.iv-zoom-slider').clientWidth;
744
+ // calculate zoom slider area
745
+ this._state.zoomSliderLength = zoomSlider - zoomHandle.offsetWidth;
746
+ }
747
+
748
+ resetZoom (animate = true) {
749
+ const { zoomValue } = this._options;
750
+
751
+ if (!animate) {
752
+ this._state.zoomValue = zoomValue;
753
+ }
754
+
755
+ this.zoom(zoomValue);
756
+ }
757
+
758
+ zoom = (perc, point) => {
759
+ const { _options, _elements, _state } = this;
760
+ const { zoomValue: curPerc, imageDim, containerDim, zoomSliderLength } = _state;
761
+ const { image, zoomHandle } = _elements;
762
+ const { maxZoom } = _options;
763
+
764
+ perc = Math.round(Math.max(100, perc));
765
+ perc = Math.min(maxZoom, perc);
766
+
767
+ point = point || {
768
+ x: containerDim.w / 2,
769
+ y: containerDim.h / 2,
770
+ };
771
+
772
+ const curLeft = parseFloat(css(image, 'left'));
773
+ const curTop = parseFloat(css(image, 'top'));
774
+
775
+ // clear any panning frames
776
+ this._clearFrames();
777
+
778
+ let step = 0;
779
+
780
+ const baseLeft = (containerDim.w - imageDim.w) / 2;
781
+ const baseTop = (containerDim.h - imageDim.h) / 2;
782
+ const baseRight = containerDim.w - baseLeft;
783
+ const baseBottom = containerDim.h - baseTop;
784
+
785
+ const zoom = () => {
786
+ step++;
787
+
788
+ if (step < 16) {
789
+ this._frames.zoomFrame = requestAnimationFrame(zoom);
790
+ }
791
+
792
+ let tickZoom = easeOutQuart(step, curPerc, perc - curPerc, 16);
793
+ // snap in at the last percent to more often land at the exact value
794
+ // only do that at the target percent value to make the animation as smooth as possible
795
+ if (Math.abs(perc - tickZoom) < 1) {
796
+ tickZoom = perc;
797
+ }
798
+ const ratio = tickZoom / curPerc;
799
+
800
+ const imgWidth = imageDim.w * tickZoom / 100;
801
+ const imgHeight = imageDim.h * tickZoom / 100;
802
+
803
+ let newLeft = -((point.x - curLeft) * ratio - point.x);
804
+ let newTop = -((point.y - curTop) * ratio - point.y);
805
+
806
+ // fix for left and top
807
+ newLeft = Math.min(newLeft, baseLeft);
808
+ newTop = Math.min(newTop, baseTop);
809
+
810
+ // fix for right and bottom
811
+ if (newLeft + imgWidth < baseRight) {
812
+ newLeft = baseRight - imgWidth; // newLeft - (newLeft + imgWidth - baseRight)
813
+ }
814
+
815
+ if (newTop + imgHeight < baseBottom) {
816
+ newTop = baseBottom - imgHeight; // newTop + (newTop + imgHeight - baseBottom)
817
+ }
818
+
819
+ css(image, {
820
+ height: `${imgHeight}px`,
821
+ width: `${imgWidth}px`,
822
+ left: `${newLeft}px`,
823
+ top: `${newTop}px`,
824
+ });
825
+
826
+ this._state.zoomValue = tickZoom;
827
+
828
+ this._resizeSnapHandle(imgWidth, imgHeight, newLeft, newTop);
829
+
830
+ // update zoom handle position
831
+ css(zoomHandle, {
832
+ left: `${(tickZoom - 100) * zoomSliderLength / (maxZoom - 100)}px`,
833
+ });
834
+
835
+ // dispatch zoom changed event
836
+ if (this._listeners.onZoomChange) {
837
+ this._listeners.onZoomChange(this._callbackData);
838
+ }
839
+ };
840
+
841
+ zoom();
842
+ };
843
+
844
+ _clearFrames = () => {
845
+ const { slideMomentumCheck, sliderMomentumFrame, zoomFrame } = this._frames;
846
+ clearInterval(slideMomentumCheck);
847
+ cancelAnimationFrame(sliderMomentumFrame);
848
+ cancelAnimationFrame(zoomFrame);
849
+ };
850
+
851
+ _resizeSnapHandle = (imgWidth, imgHeight, imgLeft, imgTop) => {
852
+ const { _elements, _state } = this;
853
+ const { snapHandle, image } = _elements;
854
+ const { imageDim, containerDim, zoomValue, snapImageDim } = _state;
855
+
856
+ const imageWidth = imgWidth || imageDim.w * zoomValue / 100;
857
+ const imageHeight = imgHeight || imageDim.h * zoomValue / 100;
858
+ const imageLeft = imgLeft || parseFloat(css(image, 'left'));
859
+ const imageTop = imgTop || parseFloat(css(image, 'top'));
860
+
861
+ const left = -imageLeft * snapImageDim.w / imageWidth;
862
+ const top = -imageTop * snapImageDim.h / imageHeight;
863
+
864
+ const handleWidth = (containerDim.w * snapImageDim.w) / imageWidth;
865
+ const handleHeight = (containerDim.h * snapImageDim.h) / imageHeight;
866
+
867
+ css(snapHandle, {
868
+ top: `${top}px`,
869
+ left: `${left}px`,
870
+ width: `${handleWidth}px`,
871
+ height: `${handleHeight}px`,
872
+ });
873
+
874
+ this._state.snapHandleDim = {
875
+ w: handleWidth,
876
+ h: handleHeight,
877
+ };
878
+ };
879
+
880
+ showSnapView = (noTimeout) => {
881
+ const { snapViewVisible, zoomValue, loaded } = this._state;
882
+ const { snapView } = this._elements;
883
+
884
+ if (!this._options.snapView) return;
885
+
886
+ if (snapViewVisible || zoomValue <= 100 || !loaded) return;
887
+
888
+ clearTimeout(this._frames.snapViewTimeout);
889
+
890
+ this._state.snapViewVisible = true;
891
+
892
+ css(snapView, { opacity: 1, pointerEvents: 'inherit' });
893
+
894
+ if (!noTimeout) {
895
+ this._frames.snapViewTimeout = setTimeout(this.hideSnapView, 1500);
896
+ }
897
+ };
898
+
899
+ hideSnapView = () => {
900
+ const { snapView } = this._elements;
901
+ css(snapView, { opacity: 0, pointerEvents: 'none' });
902
+ this._state.snapViewVisible = false;
903
+ };
904
+
905
+ refresh = (animate = true) => {
906
+ this._calculateDimensions();
907
+ this.resetZoom(animate);
908
+ };
909
+
910
+ load (imageSrc, hiResImageSrc) {
911
+ this._images = {
912
+ imageSrc,
913
+ hiResImageSrc,
914
+ };
915
+
916
+ this._loadImages();
917
+ }
918
+
919
+ destroy () {
920
+ const { container, domElement } = this._elements;
921
+ // destroy all the sliders
922
+ Object.entries(this._sliders).forEach(([, slider]) => {
923
+ slider.destroy();
924
+ });
925
+
926
+ // unbind all events
927
+ Object.entries(this._events).forEach(([, unbindEvent]) => {
928
+ unbindEvent();
929
+ });
930
+
931
+ // clear all the frames
932
+ this._clearFrames();
933
+
934
+ // remove html from the container
935
+ remove(container.querySelector('.iv-wrap'));
936
+
937
+ // remove iv-container class from container
938
+ removeClass(container, 'iv-container');
939
+
940
+ // remove added style from container
941
+ removeCss(document.querySelector('html'), 'relative');
942
+
943
+ // if container has original image, unwrap the image and remove the class
944
+ // which will happen when domElement is not the container
945
+ if (domElement !== container) {
946
+ unwrap(domElement);
947
+ }
948
+
949
+ // remove imageViewer reference from dom element
950
+ domElement._imageViewer = null;
951
+
952
+ if (this._listeners.onDestroy) {
953
+ this._listeners.onDestroy();
954
+ }
955
+ }
956
+
957
+ /**
958
+ * Data will be passed to the callback registered with each new instance
959
+ */
960
+ get _callbackData () {
961
+ return {
962
+ container: this._elements.container,
963
+ snapView: this._elements.snapView,
964
+ zoomValue: this._state.zoomValue,
965
+ reachedMin: Math.abs(this._state.zoomValue - 100) < 1,
966
+ reachedMax: Math.abs(this._state.zoomValue - this._options.maxZoom) < 1,
967
+ instance: this,
968
+ };
969
+ }
970
+ }
971
+
972
+ ImageViewer.defaults = {
973
+ zoomValue: 100,
974
+ snapView: true,
975
+ maxZoom: 500,
976
+ refreshOnResize: true,
977
+ zoomOnMouseWheel: true,
978
+ hasZoomButtons: false,
979
+ zoomStep: 50,
980
+ listeners: {
981
+ onInit: null,
982
+ onDestroy: null,
983
+ onImageLoaded: null,
984
+ onZoomChange: null,
985
+ },
986
+ };
987
+
988
+ export default ImageViewer;