• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

frontend-collective / react-image-lightbox / 3935384978

pending completion
3935384978

Pull #701

github

GitHub
Merge 2f12cc353 into f16424690
Pull Request #701: build(deps-dev): bump eslint-plugin-import from 2.23.4 to 2.27.5

141 of 318 branches covered (44.34%)

Branch coverage included in aggregate %.

257 of 556 relevant lines covered (46.22%)

8.37 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

43.27
/src/react-image-lightbox.js
1
import React, { Component } from 'react';
2
import PropTypes from 'prop-types';
3
import Modal from 'react-modal';
4
import {
5
  translate,
6
  getWindowWidth,
7
  getWindowHeight,
8
  getHighestSafeWindowContext,
9
} from './util';
10
import {
11
  KEYS,
12
  MIN_ZOOM_LEVEL,
13
  MAX_ZOOM_LEVEL,
14
  ZOOM_RATIO,
15
  WHEEL_MOVE_X_THRESHOLD,
16
  WHEEL_MOVE_Y_THRESHOLD,
17
  ZOOM_BUTTON_INCREMENT_SIZE,
18
  ACTION_NONE,
19
  ACTION_MOVE,
20
  ACTION_SWIPE,
21
  ACTION_PINCH,
22
  SOURCE_ANY,
23
  SOURCE_MOUSE,
24
  SOURCE_TOUCH,
25
  SOURCE_POINTER,
26
  MIN_SWIPE_DISTANCE,
27
} from './constant';
28
import './style.css';
29

30
class ReactImageLightbox extends Component {
31
  static isTargetMatchImage(target) {
32
    return target && /ril-image-current/.test(target.className);
×
33
  }
34

35
  static parseMouseEvent(mouseEvent) {
36
    return {
×
37
      id: 'mouse',
38
      source: SOURCE_MOUSE,
39
      x: parseInt(mouseEvent.clientX, 10),
40
      y: parseInt(mouseEvent.clientY, 10),
41
    };
42
  }
43

44
  static parseTouchPointer(touchPointer) {
45
    return {
×
46
      id: touchPointer.identifier,
47
      source: SOURCE_TOUCH,
48
      x: parseInt(touchPointer.clientX, 10),
49
      y: parseInt(touchPointer.clientY, 10),
50
    };
51
  }
52

53
  static parsePointerEvent(pointerEvent) {
54
    return {
×
55
      id: pointerEvent.pointerId,
56
      source: SOURCE_POINTER,
57
      x: parseInt(pointerEvent.clientX, 10),
58
      y: parseInt(pointerEvent.clientY, 10),
59
    };
60
  }
61

62
  // Request to transition to the previous image
63
  static getTransform({ x = 0, y = 0, zoom = 1, width, targetWidth }) {
60!
64
    let nextX = x;
59✔
65
    const windowWidth = getWindowWidth();
59✔
66
    if (width > windowWidth) {
59!
67
      nextX += (windowWidth - width) / 2;
×
68
    }
69
    const scaleFactor = zoom * (targetWidth / width);
59✔
70

71
    return {
59✔
72
      transform: `translate3d(${nextX}px,${y}px,0) scale3d(${scaleFactor},${scaleFactor},1)`,
73
    };
74
  }
75

76
  constructor(props) {
77
    super(props);
6✔
78

79
    this.state = {
6✔
80
      //-----------------------------
81
      // Animation
82
      //-----------------------------
83

84
      // Lightbox is closing
85
      // When Lightbox is mounted, if animation is enabled it will open with the reverse of the closing animation
86
      isClosing: !props.animationDisabled,
87

88
      // Component parts should animate (e.g., when images are moving, or image is being zoomed)
89
      shouldAnimate: false,
90

91
      //-----------------------------
92
      // Zoom settings
93
      //-----------------------------
94
      // Zoom level of image
95
      zoomLevel: MIN_ZOOM_LEVEL,
96

97
      //-----------------------------
98
      // Image position settings
99
      //-----------------------------
100
      // Horizontal offset from center
101
      offsetX: 0,
102

103
      // Vertical offset from center
104
      offsetY: 0,
105

106
      // image load error for srcType
107
      loadErrorStatus: {},
108
    };
109

110
    // Refs
111
    this.outerEl = React.createRef();
6✔
112
    this.zoomInBtn = React.createRef();
6✔
113
    this.zoomOutBtn = React.createRef();
6✔
114
    this.caption = React.createRef();
6✔
115

116
    this.closeIfClickInner = this.closeIfClickInner.bind(this);
6✔
117
    this.handleImageDoubleClick = this.handleImageDoubleClick.bind(this);
6✔
118
    this.handleImageMouseWheel = this.handleImageMouseWheel.bind(this);
6✔
119
    this.handleKeyInput = this.handleKeyInput.bind(this);
6✔
120
    this.handleMouseUp = this.handleMouseUp.bind(this);
6✔
121
    this.handleMouseDown = this.handleMouseDown.bind(this);
6✔
122
    this.handleMouseMove = this.handleMouseMove.bind(this);
6✔
123
    this.handleOuterMousewheel = this.handleOuterMousewheel.bind(this);
6✔
124
    this.handleTouchStart = this.handleTouchStart.bind(this);
6✔
125
    this.handleTouchMove = this.handleTouchMove.bind(this);
6✔
126
    this.handleTouchEnd = this.handleTouchEnd.bind(this);
6✔
127
    this.handlePointerEvent = this.handlePointerEvent.bind(this);
6✔
128
    this.handleCaptionMousewheel = this.handleCaptionMousewheel.bind(this);
6✔
129
    this.handleWindowResize = this.handleWindowResize.bind(this);
6✔
130
    this.handleZoomInButtonClick = this.handleZoomInButtonClick.bind(this);
6✔
131
    this.handleZoomOutButtonClick = this.handleZoomOutButtonClick.bind(this);
6✔
132
    this.requestClose = this.requestClose.bind(this);
6✔
133
    this.requestMoveNext = this.requestMoveNext.bind(this);
6✔
134
    this.requestMovePrev = this.requestMovePrev.bind(this);
6✔
135

136
    // Timeouts - always clear it before umount
137
    this.timeouts = [];
6✔
138

139
    // Current action
140
    this.currentAction = ACTION_NONE;
6✔
141

142
    // Events source
143
    this.eventsSource = SOURCE_ANY;
6✔
144

145
    // Empty pointers list
146
    this.pointerList = [];
6✔
147

148
    // Prevent inner close
149
    this.preventInnerClose = false;
6✔
150
    this.preventInnerCloseTimeout = null;
6✔
151

152
    // Used to disable animation when changing props.mainSrc|nextSrc|prevSrc
153
    this.keyPressed = false;
6✔
154

155
    // Used to store load state / dimensions of images
156
    this.imageCache = {};
6✔
157

158
    // Time the last keydown event was called (used in keyboard action rate limiting)
159
    this.lastKeyDownTime = 0;
6✔
160

161
    // Used for debouncing window resize event
162
    this.resizeTimeout = null;
6✔
163

164
    // Used to determine when actions are triggered by the scroll wheel
165
    this.wheelActionTimeout = null;
6✔
166
    this.resetScrollTimeout = null;
6✔
167
    this.scrollX = 0;
6✔
168
    this.scrollY = 0;
6✔
169

170
    // Used in panning zoomed images
171
    this.moveStartX = 0;
6✔
172
    this.moveStartY = 0;
6✔
173
    this.moveStartOffsetX = 0;
6✔
174
    this.moveStartOffsetY = 0;
6✔
175

176
    // Used to swipe
177
    this.swipeStartX = 0;
6✔
178
    this.swipeStartY = 0;
6✔
179
    this.swipeEndX = 0;
6✔
180
    this.swipeEndY = 0;
6✔
181

182
    // Used to pinch
183
    this.pinchTouchList = null;
6✔
184
    this.pinchDistance = 0;
6✔
185

186
    // Used to differentiate between images with identical src
187
    this.keyCounter = 0;
6✔
188

189
    // Used to detect a move when all src's remain unchanged (four or more of the same image in a row)
190
    this.moveRequested = false;
6✔
191
  }
192

193
  componentDidMount() {
194
    if (!this.props.animationDisabled) {
6✔
195
      // Make opening animation play
196
      this.setState({ isClosing: false });
5✔
197
    }
198

199
    // Prevents cross-origin errors when using a cross-origin iframe
200
    this.windowContext = getHighestSafeWindowContext();
6✔
201

202
    this.listeners = {
6✔
203
      resize: this.handleWindowResize,
204
      mouseup: this.handleMouseUp,
205
      touchend: this.handleTouchEnd,
206
      touchcancel: this.handleTouchEnd,
207
      pointerdown: this.handlePointerEvent,
208
      pointermove: this.handlePointerEvent,
209
      pointerup: this.handlePointerEvent,
210
      pointercancel: this.handlePointerEvent,
211
    };
212
    Object.keys(this.listeners).forEach(type => {
6✔
213
      this.windowContext.addEventListener(type, this.listeners[type]);
48✔
214
    });
215

216
    this.loadAllImages();
6✔
217
  }
218

219
  shouldComponentUpdate(nextProps) {
220
    this.getSrcTypes().forEach(srcType => {
26✔
221
      if (this.props[srcType.name] !== nextProps[srcType.name]) {
156✔
222
        this.moveRequested = false;
6✔
223
      }
224
    });
225

226
    // Wait for move...
227
    return !this.moveRequested;
26✔
228
  }
229

230
  componentDidUpdate(prevProps) {
231
    let sourcesChanged = false;
23✔
232
    const prevSrcDict = {};
23✔
233
    const nextSrcDict = {};
23✔
234
    this.getSrcTypes().forEach(srcType => {
23✔
235
      if (prevProps[srcType.name] !== this.props[srcType.name]) {
138✔
236
        sourcesChanged = true;
6✔
237

238
        prevSrcDict[prevProps[srcType.name]] = true;
6✔
239
        nextSrcDict[this.props[srcType.name]] = true;
6✔
240
      }
241
    });
242

243
    if (sourcesChanged || this.moveRequested) {
23✔
244
      // Reset the loaded state for images not rendered next
245
      Object.keys(prevSrcDict).forEach(prevSrc => {
6✔
246
        if (!(prevSrc in nextSrcDict) && prevSrc in this.imageCache) {
6✔
247
          this.imageCache[prevSrc].loaded = false;
1✔
248
        }
249
      });
250

251
      this.moveRequested = false;
6✔
252

253
      // Load any new images
254
      this.loadAllImages(this.props);
6✔
255
    }
256
  }
257

258
  componentWillUnmount() {
259
    this.didUnmount = true;
×
260
    Object.keys(this.listeners).forEach(type => {
×
261
      this.windowContext.removeEventListener(type, this.listeners[type]);
×
262
    });
263
    this.timeouts.forEach(tid => clearTimeout(tid));
×
264
  }
265

266
  setTimeout(func, time) {
267
    const id = setTimeout(() => {
×
268
      this.timeouts = this.timeouts.filter(tid => tid !== id);
×
269
      func();
×
270
    }, time);
271
    this.timeouts.push(id);
×
272
    return id;
×
273
  }
274

275
  setPreventInnerClose() {
276
    if (this.preventInnerCloseTimeout) {
×
277
      this.clearTimeout(this.preventInnerCloseTimeout);
×
278
    }
279
    this.preventInnerClose = true;
×
280
    this.preventInnerCloseTimeout = this.setTimeout(() => {
×
281
      this.preventInnerClose = false;
×
282
      this.preventInnerCloseTimeout = null;
×
283
    }, 100);
284
  }
285

286
  // Get info for the best suited image to display with the given srcType
287
  getBestImageForType(srcType) {
288
    let imageSrc = this.props[srcType];
60✔
289
    let fitSizes = {};
60✔
290

291
    if (this.isImageLoaded(imageSrc)) {
60✔
292
      // Use full-size image if available
293
      fitSizes = this.getFitSizes(
1✔
294
        this.imageCache[imageSrc].width,
295
        this.imageCache[imageSrc].height
296
      );
297
    } else if (this.isImageLoaded(this.props[`${srcType}Thumbnail`])) {
59!
298
      // Fall back to using thumbnail if the image has not been loaded
299
      imageSrc = this.props[`${srcType}Thumbnail`];
×
300
      fitSizes = this.getFitSizes(
×
301
        this.imageCache[imageSrc].width,
302
        this.imageCache[imageSrc].height,
303
        true
304
      );
305
    } else {
306
      return null;
59✔
307
    }
308

309
    return {
1✔
310
      src: imageSrc,
311
      height: this.imageCache[imageSrc].height,
312
      width: this.imageCache[imageSrc].width,
313
      targetHeight: fitSizes.height,
314
      targetWidth: fitSizes.width,
315
    };
316
  }
317

318
  // Get sizing for when an image is larger than the window
319
  getFitSizes(width, height, stretch) {
320
    const boxSize = this.getLightboxRect();
1✔
321
    let maxHeight = boxSize.height - this.props.imagePadding * 2;
1✔
322
    let maxWidth = boxSize.width - this.props.imagePadding * 2;
1✔
323

324
    if (!stretch) {
1!
325
      maxHeight = Math.min(maxHeight, height);
1✔
326
      maxWidth = Math.min(maxWidth, width);
1✔
327
    }
328

329
    const maxRatio = maxWidth / maxHeight;
1✔
330
    const srcRatio = width / height;
1✔
331

332
    if (maxRatio > srcRatio) {
1!
333
      // height is the constraining dimension of the photo
334
      return {
×
335
        width: (width * maxHeight) / height,
336
        height: maxHeight,
337
      };
338
    }
339

340
    return {
1✔
341
      width: maxWidth,
342
      height: (height * maxWidth) / width,
343
    };
344
  }
345

346
  getMaxOffsets(zoomLevel = this.state.zoomLevel) {
×
347
    const currentImageInfo = this.getBestImageForType('mainSrc');
×
348
    if (currentImageInfo === null) {
×
349
      return { maxX: 0, minX: 0, maxY: 0, minY: 0 };
×
350
    }
351

352
    const boxSize = this.getLightboxRect();
×
353
    const zoomMultiplier = this.getZoomMultiplier(zoomLevel);
×
354

355
    let maxX = 0;
×
356
    if (zoomMultiplier * currentImageInfo.width - boxSize.width < 0) {
×
357
      // if there is still blank space in the X dimension, don't limit except to the opposite edge
358
      maxX = (boxSize.width - zoomMultiplier * currentImageInfo.width) / 2;
×
359
    } else {
360
      maxX = (zoomMultiplier * currentImageInfo.width - boxSize.width) / 2;
×
361
    }
362

363
    let maxY = 0;
×
364
    if (zoomMultiplier * currentImageInfo.height - boxSize.height < 0) {
×
365
      // if there is still blank space in the Y dimension, don't limit except to the opposite edge
366
      maxY = (boxSize.height - zoomMultiplier * currentImageInfo.height) / 2;
×
367
    } else {
368
      maxY = (zoomMultiplier * currentImageInfo.height - boxSize.height) / 2;
×
369
    }
370

371
    return {
×
372
      maxX,
373
      maxY,
374
      minX: -1 * maxX,
375
      minY: -1 * maxY,
376
    };
377
  }
378

379
  // Get image src types
380
  getSrcTypes() {
381
    return [
90✔
382
      {
383
        name: 'mainSrc',
384
        keyEnding: `i${this.keyCounter}`,
385
      },
386
      {
387
        name: 'mainSrcThumbnail',
388
        keyEnding: `t${this.keyCounter}`,
389
      },
390
      {
391
        name: 'nextSrc',
392
        keyEnding: `i${this.keyCounter + 1}`,
393
      },
394
      {
395
        name: 'nextSrcThumbnail',
396
        keyEnding: `t${this.keyCounter + 1}`,
397
      },
398
      {
399
        name: 'prevSrc',
400
        keyEnding: `i${this.keyCounter - 1}`,
401
      },
402
      {
403
        name: 'prevSrcThumbnail',
404
        keyEnding: `t${this.keyCounter - 1}`,
405
      },
406
    ];
407
  }
408

409
  /**
410
   * Get sizing when the image is scaled
411
   */
412
  getZoomMultiplier(zoomLevel = this.state.zoomLevel) {
29✔
413
    return ZOOM_RATIO ** zoomLevel;
29✔
414
  }
415

416
  /**
417
   * Get the size of the lightbox in pixels
418
   */
419
  getLightboxRect() {
420
    if (this.outerEl.current) {
30✔
421
      return this.outerEl.current.getBoundingClientRect();
19✔
422
    }
423

424
    return {
11✔
425
      width: getWindowWidth(),
426
      height: getWindowHeight(),
427
      top: 0,
428
      right: 0,
429
      bottom: 0,
430
      left: 0,
431
    };
432
  }
433

434
  clearTimeout(id) {
435
    this.timeouts = this.timeouts.filter(tid => tid !== id);
×
436
    clearTimeout(id);
×
437
  }
438

439
  // Change zoom level
440
  changeZoom(zoomLevel, clientX, clientY) {
441
    // Ignore if zoom disabled
442
    if (!this.props.enableZoom) {
2!
443
      return;
×
444
    }
445

446
    // Constrain zoom level to the set bounds
447
    const nextZoomLevel = Math.max(
2✔
448
      MIN_ZOOM_LEVEL,
449
      Math.min(MAX_ZOOM_LEVEL, zoomLevel)
450
    );
451

452
    // Ignore requests that don't change the zoom level
453
    if (nextZoomLevel === this.state.zoomLevel) {
2!
454
      return;
×
455
    }
456

457
    if (nextZoomLevel === MIN_ZOOM_LEVEL) {
2✔
458
      // Snap back to center if zoomed all the way out
459
      this.setState({
1✔
460
        zoomLevel: nextZoomLevel,
461
        offsetX: 0,
462
        offsetY: 0,
463
      });
464

465
      return;
1✔
466
    }
467

468
    const imageBaseSize = this.getBestImageForType('mainSrc');
1✔
469
    if (imageBaseSize === null) {
1!
470
      return;
1✔
471
    }
472

473
    const currentZoomMultiplier = this.getZoomMultiplier();
×
474
    const nextZoomMultiplier = this.getZoomMultiplier(nextZoomLevel);
×
475

476
    // Default to the center of the image to zoom when no mouse position specified
477
    const boxRect = this.getLightboxRect();
×
478
    const pointerX =
479
      typeof clientX !== 'undefined'
×
480
        ? clientX - boxRect.left
481
        : boxRect.width / 2;
482
    const pointerY =
483
      typeof clientY !== 'undefined'
×
484
        ? clientY - boxRect.top
485
        : boxRect.height / 2;
486

487
    const currentImageOffsetX =
488
      (boxRect.width - imageBaseSize.width * currentZoomMultiplier) / 2;
×
489
    const currentImageOffsetY =
490
      (boxRect.height - imageBaseSize.height * currentZoomMultiplier) / 2;
×
491

492
    const currentImageRealOffsetX = currentImageOffsetX - this.state.offsetX;
×
493
    const currentImageRealOffsetY = currentImageOffsetY - this.state.offsetY;
×
494

495
    const currentPointerXRelativeToImage =
496
      (pointerX - currentImageRealOffsetX) / currentZoomMultiplier;
×
497
    const currentPointerYRelativeToImage =
498
      (pointerY - currentImageRealOffsetY) / currentZoomMultiplier;
×
499

500
    const nextImageRealOffsetX =
501
      pointerX - currentPointerXRelativeToImage * nextZoomMultiplier;
×
502
    const nextImageRealOffsetY =
503
      pointerY - currentPointerYRelativeToImage * nextZoomMultiplier;
×
504

505
    const nextImageOffsetX =
506
      (boxRect.width - imageBaseSize.width * nextZoomMultiplier) / 2;
×
507
    const nextImageOffsetY =
508
      (boxRect.height - imageBaseSize.height * nextZoomMultiplier) / 2;
×
509

510
    let nextOffsetX = nextImageOffsetX - nextImageRealOffsetX;
×
511
    let nextOffsetY = nextImageOffsetY - nextImageRealOffsetY;
×
512

513
    // When zooming out, limit the offset so things don't get left askew
514
    if (this.currentAction !== ACTION_PINCH) {
×
515
      const maxOffsets = this.getMaxOffsets();
×
516
      if (this.state.zoomLevel > nextZoomLevel) {
×
517
        nextOffsetX = Math.max(
×
518
          maxOffsets.minX,
519
          Math.min(maxOffsets.maxX, nextOffsetX)
520
        );
521
        nextOffsetY = Math.max(
×
522
          maxOffsets.minY,
523
          Math.min(maxOffsets.maxY, nextOffsetY)
524
        );
525
      }
526
    }
527

528
    this.setState({
×
529
      zoomLevel: nextZoomLevel,
530
      offsetX: nextOffsetX,
531
      offsetY: nextOffsetY,
532
    });
533
  }
534

535
  closeIfClickInner(event) {
536
    if (
×
537
      !this.preventInnerClose &&
×
538
      event.target.className.search(/\bril-inner\b/) > -1
539
    ) {
540
      this.requestClose(event);
×
541
    }
542
  }
543

544
  /**
545
   * Handle user keyboard actions
546
   */
547
  handleKeyInput(event) {
548
    event.stopPropagation();
5✔
549

550
    // Ignore key input during animations
551
    if (this.isAnimating()) {
5!
552
      return;
×
553
    }
554

555
    // Allow slightly faster navigation through the images when user presses keys repeatedly
556
    if (event.type === 'keyup') {
5!
557
      this.lastKeyDownTime -= this.props.keyRepeatKeyupBonus;
×
558
      return;
×
559
    }
560

561
    const keyCode = event.which || event.keyCode;
5✔
562

563
    // Ignore key presses that happen too close to each other (when rapid fire key pressing or holding down the key)
564
    // But allow it if it's a lightbox closing action
565
    const currentTime = new Date();
5✔
566
    if (
5!
567
      currentTime.getTime() - this.lastKeyDownTime <
5!
568
        this.props.keyRepeatLimit &&
569
      keyCode !== KEYS.ESC
570
    ) {
571
      return;
×
572
    }
573
    this.lastKeyDownTime = currentTime.getTime();
5✔
574

575
    switch (keyCode) {
5!
576
      // ESC key closes the lightbox
577
      case KEYS.ESC:
578
        event.preventDefault();
1✔
579
        this.requestClose(event);
1✔
580
        break;
1✔
581

582
      // Left arrow key moves to previous image
583
      case KEYS.LEFT_ARROW:
584
        if (!this.props.prevSrc) {
2✔
585
          return;
1✔
586
        }
587

588
        event.preventDefault();
1✔
589
        this.keyPressed = true;
1✔
590
        this.requestMovePrev(event);
1✔
591
        break;
1✔
592

593
      // Right arrow key moves to next image
594
      case KEYS.RIGHT_ARROW:
595
        if (!this.props.nextSrc) {
2✔
596
          return;
1✔
597
        }
598

599
        event.preventDefault();
1✔
600
        this.keyPressed = true;
1✔
601
        this.requestMoveNext(event);
1✔
602
        break;
1✔
603

604
      default:
605
    }
606
  }
607

608
  /**
609
   * Handle a mouse wheel event over the lightbox container
610
   */
611
  handleOuterMousewheel(event) {
612
    // Prevent scrolling of the background
613
    event.stopPropagation();
×
614

615
    const xThreshold = WHEEL_MOVE_X_THRESHOLD;
×
616
    let actionDelay = 0;
×
617
    const imageMoveDelay = 500;
×
618

619
    this.clearTimeout(this.resetScrollTimeout);
×
620
    this.resetScrollTimeout = this.setTimeout(() => {
×
621
      this.scrollX = 0;
×
622
      this.scrollY = 0;
×
623
    }, 300);
624

625
    // Prevent rapid-fire zoom behavior
626
    if (this.wheelActionTimeout !== null || this.isAnimating()) {
×
627
      return;
×
628
    }
629

630
    if (Math.abs(event.deltaY) < Math.abs(event.deltaX)) {
×
631
      // handle horizontal scrolls with image moves
632
      this.scrollY = 0;
×
633
      this.scrollX += event.deltaX;
×
634

635
      const bigLeapX = xThreshold / 2;
×
636
      // If the scroll amount has accumulated sufficiently, or a large leap was taken
637
      if (this.scrollX >= xThreshold || event.deltaX >= bigLeapX) {
×
638
        // Scroll right moves to next
639
        this.requestMoveNext(event);
×
640
        actionDelay = imageMoveDelay;
×
641
        this.scrollX = 0;
×
642
      } else if (
×
643
        this.scrollX <= -1 * xThreshold ||
×
644
        event.deltaX <= -1 * bigLeapX
645
      ) {
646
        // Scroll left moves to previous
647
        this.requestMovePrev(event);
×
648
        actionDelay = imageMoveDelay;
×
649
        this.scrollX = 0;
×
650
      }
651
    }
652

653
    // Allow successive actions after the set delay
654
    if (actionDelay !== 0) {
×
655
      this.wheelActionTimeout = this.setTimeout(() => {
×
656
        this.wheelActionTimeout = null;
×
657
      }, actionDelay);
658
    }
659
  }
660

661
  handleImageMouseWheel(event) {
662
    const yThreshold = WHEEL_MOVE_Y_THRESHOLD;
×
663

664
    if (Math.abs(event.deltaY) >= Math.abs(event.deltaX)) {
×
665
      event.stopPropagation();
×
666
      // If the vertical scroll amount was large enough, perform a zoom
667
      if (Math.abs(event.deltaY) < yThreshold) {
×
668
        return;
×
669
      }
670

671
      this.scrollX = 0;
×
672
      this.scrollY += event.deltaY;
×
673

674
      this.changeZoom(
×
675
        this.state.zoomLevel - event.deltaY,
676
        event.clientX,
677
        event.clientY
678
      );
679
    }
680
  }
681

682
  /**
683
   * Handle a double click on the current image
684
   */
685
  handleImageDoubleClick(event) {
686
    if (this.state.zoomLevel > MIN_ZOOM_LEVEL) {
×
687
      // A double click when zoomed in zooms all the way out
688
      this.changeZoom(MIN_ZOOM_LEVEL, event.clientX, event.clientY);
×
689
    } else {
690
      // A double click when zoomed all the way out zooms in
691
      this.changeZoom(
×
692
        this.state.zoomLevel + ZOOM_BUTTON_INCREMENT_SIZE,
693
        event.clientX,
694
        event.clientY
695
      );
696
    }
697
  }
698

699
  shouldHandleEvent(source) {
700
    if (this.eventsSource === source) {
×
701
      return true;
×
702
    }
703
    if (this.eventsSource === SOURCE_ANY) {
×
704
      this.eventsSource = source;
×
705
      return true;
×
706
    }
707
    switch (source) {
×
708
      case SOURCE_MOUSE:
709
        return false;
×
710
      case SOURCE_TOUCH:
711
        this.eventsSource = SOURCE_TOUCH;
×
712
        this.filterPointersBySource();
×
713
        return true;
×
714
      case SOURCE_POINTER:
715
        if (this.eventsSource === SOURCE_MOUSE) {
×
716
          this.eventsSource = SOURCE_POINTER;
×
717
          this.filterPointersBySource();
×
718
          return true;
×
719
        }
720
        return false;
×
721
      default:
722
        return false;
×
723
    }
724
  }
725

726
  addPointer(pointer) {
727
    this.pointerList.push(pointer);
×
728
  }
729

730
  removePointer(pointer) {
731
    this.pointerList = this.pointerList.filter(({ id }) => id !== pointer.id);
×
732
  }
733

734
  filterPointersBySource() {
735
    this.pointerList = this.pointerList.filter(
×
736
      ({ source }) => source === this.eventsSource
×
737
    );
738
  }
739

740
  handleMouseDown(event) {
741
    if (
×
742
      this.shouldHandleEvent(SOURCE_MOUSE) &&
×
743
      ReactImageLightbox.isTargetMatchImage(event.target)
744
    ) {
745
      this.addPointer(ReactImageLightbox.parseMouseEvent(event));
×
746
      this.multiPointerStart(event);
×
747
    }
748
  }
749

750
  handleMouseMove(event) {
751
    if (this.shouldHandleEvent(SOURCE_MOUSE)) {
×
752
      this.multiPointerMove(event, [ReactImageLightbox.parseMouseEvent(event)]);
×
753
    }
754
  }
755

756
  handleMouseUp(event) {
757
    if (this.shouldHandleEvent(SOURCE_MOUSE)) {
×
758
      this.removePointer(ReactImageLightbox.parseMouseEvent(event));
×
759
      this.multiPointerEnd(event);
×
760
    }
761
  }
762

763
  handlePointerEvent(event) {
764
    if (this.shouldHandleEvent(SOURCE_POINTER)) {
×
765
      switch (event.type) {
×
766
        case 'pointerdown':
767
          if (ReactImageLightbox.isTargetMatchImage(event.target)) {
×
768
            this.addPointer(ReactImageLightbox.parsePointerEvent(event));
×
769
            this.multiPointerStart(event);
×
770
          }
771
          break;
×
772
        case 'pointermove':
773
          this.multiPointerMove(event, [
×
774
            ReactImageLightbox.parsePointerEvent(event),
775
          ]);
776
          break;
×
777
        case 'pointerup':
778
        case 'pointercancel':
779
          this.removePointer(ReactImageLightbox.parsePointerEvent(event));
×
780
          this.multiPointerEnd(event);
×
781
          break;
×
782
        default:
783
          break;
×
784
      }
785
    }
786
  }
787

788
  handleTouchStart(event) {
789
    if (
×
790
      this.shouldHandleEvent(SOURCE_TOUCH) &&
×
791
      ReactImageLightbox.isTargetMatchImage(event.target)
792
    ) {
793
      [].forEach.call(event.changedTouches, eventTouch =>
×
794
        this.addPointer(ReactImageLightbox.parseTouchPointer(eventTouch))
×
795
      );
796
      this.multiPointerStart(event);
×
797
    }
798
  }
799

800
  handleTouchMove(event) {
801
    if (this.shouldHandleEvent(SOURCE_TOUCH)) {
×
802
      this.multiPointerMove(
×
803
        event,
804
        [].map.call(event.changedTouches, eventTouch =>
805
          ReactImageLightbox.parseTouchPointer(eventTouch)
×
806
        )
807
      );
808
    }
809
  }
810

811
  handleTouchEnd(event) {
812
    if (this.shouldHandleEvent(SOURCE_TOUCH)) {
×
813
      [].map.call(event.changedTouches, touch =>
×
814
        this.removePointer(ReactImageLightbox.parseTouchPointer(touch))
×
815
      );
816
      this.multiPointerEnd(event);
×
817
    }
818
  }
819

820
  decideMoveOrSwipe(pointer) {
821
    if (this.state.zoomLevel <= MIN_ZOOM_LEVEL) {
×
822
      this.handleSwipeStart(pointer);
×
823
    } else {
824
      this.handleMoveStart(pointer);
×
825
    }
826
  }
827

828
  multiPointerStart(event) {
829
    this.handleEnd(null);
×
830
    switch (this.pointerList.length) {
×
831
      case 1: {
832
        event.preventDefault();
×
833
        this.decideMoveOrSwipe(this.pointerList[0]);
×
834
        break;
×
835
      }
836
      case 2: {
837
        event.preventDefault();
×
838
        this.handlePinchStart(this.pointerList);
×
839
        break;
×
840
      }
841
      default:
842
        break;
×
843
    }
844
  }
845

846
  multiPointerMove(event, pointerList) {
847
    switch (this.currentAction) {
×
848
      case ACTION_MOVE: {
849
        event.preventDefault();
×
850
        this.handleMove(pointerList[0]);
×
851
        break;
×
852
      }
853
      case ACTION_SWIPE: {
854
        event.preventDefault();
×
855
        this.handleSwipe(pointerList[0]);
×
856
        break;
×
857
      }
858
      case ACTION_PINCH: {
859
        event.preventDefault();
×
860
        this.handlePinch(pointerList);
×
861
        break;
×
862
      }
863
      default:
864
        break;
×
865
    }
866
  }
867

868
  multiPointerEnd(event) {
869
    if (this.currentAction !== ACTION_NONE) {
×
870
      this.setPreventInnerClose();
×
871
      this.handleEnd(event);
×
872
    }
873
    switch (this.pointerList.length) {
×
874
      case 0: {
875
        this.eventsSource = SOURCE_ANY;
×
876
        break;
×
877
      }
878
      case 1: {
879
        event.preventDefault();
×
880
        this.decideMoveOrSwipe(this.pointerList[0]);
×
881
        break;
×
882
      }
883
      case 2: {
884
        event.preventDefault();
×
885
        this.handlePinchStart(this.pointerList);
×
886
        break;
×
887
      }
888
      default:
889
        break;
×
890
    }
891
  }
892

893
  handleEnd(event) {
894
    switch (this.currentAction) {
×
895
      case ACTION_MOVE:
896
        this.handleMoveEnd(event);
×
897
        break;
×
898
      case ACTION_SWIPE:
899
        this.handleSwipeEnd(event);
×
900
        break;
×
901
      case ACTION_PINCH:
902
        this.handlePinchEnd(event);
×
903
        break;
×
904
      default:
905
        break;
×
906
    }
907
  }
908

909
  // Handle move start over the lightbox container
910
  // This happens:
911
  // - On a mouseDown event
912
  // - On a touchstart event
913
  handleMoveStart({ x: clientX, y: clientY }) {
914
    if (!this.props.enableZoom) {
×
915
      return;
×
916
    }
917
    this.currentAction = ACTION_MOVE;
×
918
    this.moveStartX = clientX;
×
919
    this.moveStartY = clientY;
×
920
    this.moveStartOffsetX = this.state.offsetX;
×
921
    this.moveStartOffsetY = this.state.offsetY;
×
922
  }
923

924
  // Handle dragging over the lightbox container
925
  // This happens:
926
  // - After a mouseDown and before a mouseUp event
927
  // - After a touchstart and before a touchend event
928
  handleMove({ x: clientX, y: clientY }) {
929
    const newOffsetX = this.moveStartX - clientX + this.moveStartOffsetX;
×
930
    const newOffsetY = this.moveStartY - clientY + this.moveStartOffsetY;
×
931
    if (
×
932
      this.state.offsetX !== newOffsetX ||
×
933
      this.state.offsetY !== newOffsetY
934
    ) {
935
      this.setState({
×
936
        offsetX: newOffsetX,
937
        offsetY: newOffsetY,
938
      });
939
    }
940
  }
941

942
  handleMoveEnd() {
943
    this.currentAction = ACTION_NONE;
×
944
    this.moveStartX = 0;
×
945
    this.moveStartY = 0;
×
946
    this.moveStartOffsetX = 0;
×
947
    this.moveStartOffsetY = 0;
×
948
    // Snap image back into frame if outside max offset range
949
    const maxOffsets = this.getMaxOffsets();
×
950
    const nextOffsetX = Math.max(
×
951
      maxOffsets.minX,
952
      Math.min(maxOffsets.maxX, this.state.offsetX)
953
    );
954
    const nextOffsetY = Math.max(
×
955
      maxOffsets.minY,
956
      Math.min(maxOffsets.maxY, this.state.offsetY)
957
    );
958
    if (
×
959
      nextOffsetX !== this.state.offsetX ||
×
960
      nextOffsetY !== this.state.offsetY
961
    ) {
962
      this.setState({
×
963
        offsetX: nextOffsetX,
964
        offsetY: nextOffsetY,
965
        shouldAnimate: true,
966
      });
967
      this.setTimeout(() => {
×
968
        this.setState({ shouldAnimate: false });
×
969
      }, this.props.animationDuration);
970
    }
971
  }
972

973
  handleSwipeStart({ x: clientX, y: clientY }) {
974
    this.currentAction = ACTION_SWIPE;
×
975
    this.swipeStartX = clientX;
×
976
    this.swipeStartY = clientY;
×
977
    this.swipeEndX = clientX;
×
978
    this.swipeEndY = clientY;
×
979
  }
980

981
  handleSwipe({ x: clientX, y: clientY }) {
982
    this.swipeEndX = clientX;
×
983
    this.swipeEndY = clientY;
×
984
  }
985

986
  handleSwipeEnd(event) {
987
    const xDiff = this.swipeEndX - this.swipeStartX;
×
988
    const xDiffAbs = Math.abs(xDiff);
×
989
    const yDiffAbs = Math.abs(this.swipeEndY - this.swipeStartY);
×
990

991
    this.currentAction = ACTION_NONE;
×
992
    this.swipeStartX = 0;
×
993
    this.swipeStartY = 0;
×
994
    this.swipeEndX = 0;
×
995
    this.swipeEndY = 0;
×
996

997
    if (!event || this.isAnimating() || xDiffAbs < yDiffAbs * 1.5) {
×
998
      return;
×
999
    }
1000

1001
    if (xDiffAbs < MIN_SWIPE_DISTANCE) {
×
1002
      const boxRect = this.getLightboxRect();
×
1003
      if (xDiffAbs < boxRect.width / 4) {
×
1004
        return;
×
1005
      }
1006
    }
1007

1008
    if (xDiff > 0 && this.props.prevSrc) {
×
1009
      event.preventDefault();
×
1010
      this.requestMovePrev();
×
1011
    } else if (xDiff < 0 && this.props.nextSrc) {
×
1012
      event.preventDefault();
×
1013
      this.requestMoveNext();
×
1014
    }
1015
  }
1016

1017
  calculatePinchDistance([a, b] = this.pinchTouchList) {
×
1018
    return Math.sqrt((a.x - b.x) ** 2 + (a.y - b.y) ** 2);
×
1019
  }
1020

1021
  calculatePinchCenter([a, b] = this.pinchTouchList) {
×
1022
    return {
×
1023
      x: a.x - (a.x - b.x) / 2,
1024
      y: a.y - (a.y - b.y) / 2,
1025
    };
1026
  }
1027

1028
  handlePinchStart(pointerList) {
1029
    if (!this.props.enableZoom) {
×
1030
      return;
×
1031
    }
1032
    this.currentAction = ACTION_PINCH;
×
1033
    this.pinchTouchList = pointerList.map(({ id, x, y }) => ({ id, x, y }));
×
1034
    this.pinchDistance = this.calculatePinchDistance();
×
1035
  }
1036

1037
  handlePinch(pointerList) {
1038
    this.pinchTouchList = this.pinchTouchList.map(oldPointer => {
×
1039
      for (let i = 0; i < pointerList.length; i += 1) {
×
1040
        if (pointerList[i].id === oldPointer.id) {
×
1041
          return pointerList[i];
×
1042
        }
1043
      }
1044

1045
      return oldPointer;
×
1046
    });
1047

1048
    const newDistance = this.calculatePinchDistance();
×
1049

1050
    const zoomLevel = this.state.zoomLevel + newDistance - this.pinchDistance;
×
1051

1052
    this.pinchDistance = newDistance;
×
1053
    const { x: clientX, y: clientY } = this.calculatePinchCenter(
×
1054
      this.pinchTouchList
1055
    );
1056
    this.changeZoom(zoomLevel, clientX, clientY);
×
1057
  }
1058

1059
  handlePinchEnd() {
1060
    this.currentAction = ACTION_NONE;
×
1061
    this.pinchTouchList = null;
×
1062
    this.pinchDistance = 0;
×
1063
  }
1064

1065
  // Handle the window resize event
1066
  handleWindowResize() {
1067
    this.clearTimeout(this.resizeTimeout);
×
1068
    this.resizeTimeout = this.setTimeout(this.forceUpdate.bind(this), 100);
×
1069
  }
1070

1071
  handleZoomInButtonClick() {
1072
    const nextZoomLevel = this.state.zoomLevel + ZOOM_BUTTON_INCREMENT_SIZE;
1✔
1073
    this.changeZoom(nextZoomLevel);
1✔
1074
    if (nextZoomLevel === MAX_ZOOM_LEVEL) {
1!
1075
      this.zoomOutBtn.current.focus();
1✔
1076
    }
1077
  }
1078

1079
  handleZoomOutButtonClick() {
1080
    const nextZoomLevel = this.state.zoomLevel - ZOOM_BUTTON_INCREMENT_SIZE;
1✔
1081
    this.changeZoom(nextZoomLevel);
1✔
1082
    if (nextZoomLevel === MIN_ZOOM_LEVEL) {
1!
1083
      this.zoomInBtn.current.focus();
1✔
1084
    }
1085
  }
1086

1087
  handleCaptionMousewheel(event) {
1088
    event.stopPropagation();
×
1089

1090
    if (!this.caption.current) {
×
1091
      return;
×
1092
    }
1093

1094
    const { height } = this.caption.current.getBoundingClientRect();
×
1095
    const { scrollHeight, scrollTop } = this.caption.current;
×
1096
    if (
×
1097
      (event.deltaY > 0 && height + scrollTop >= scrollHeight) ||
×
1098
      (event.deltaY < 0 && scrollTop <= 0)
1099
    ) {
1100
      event.preventDefault();
×
1101
    }
1102
  }
1103

1104
  // Detach key and mouse input events
1105
  isAnimating() {
1106
    return this.state.shouldAnimate || this.state.isClosing;
185✔
1107
  }
1108

1109
  // Check if image is loaded
1110
  isImageLoaded(imageSrc) {
1111
    return (
167✔
1112
      imageSrc &&
276✔
1113
      imageSrc in this.imageCache &&
1114
      this.imageCache[imageSrc].loaded
1115
    );
1116
  }
1117

1118
  // Load image from src and call callback with image width and height on load
1119
  loadImage(srcType, imageSrc, done) {
1120
    // Return the image info if it is already cached
1121
    if (this.isImageLoaded(imageSrc)) {
24!
1122
      this.setTimeout(() => {
×
1123
        done();
×
1124
      }, 1);
1125
      return;
×
1126
    }
1127

1128
    const inMemoryImage = new global.Image();
24✔
1129

1130
    if (this.props.imageCrossOrigin) {
24!
1131
      inMemoryImage.crossOrigin = this.props.imageCrossOrigin;
×
1132
    }
1133

1134
    inMemoryImage.onerror = errorEvent => {
24✔
1135
      this.props.onImageLoadError(imageSrc, srcType, errorEvent);
1✔
1136

1137
      // failed to load so set the state loadErrorStatus
1138
      this.setState(prevState => ({
1✔
1139
        loadErrorStatus: { ...prevState.loadErrorStatus, [srcType]: true },
1140
      }));
1141

1142
      done(errorEvent);
1✔
1143
    };
1144

1145
    inMemoryImage.onload = () => {
24✔
1146
      this.props.onImageLoad(imageSrc, srcType, inMemoryImage);
1✔
1147

1148
      this.imageCache[imageSrc] = {
1✔
1149
        loaded: true,
1150
        width: inMemoryImage.width,
1151
        height: inMemoryImage.height,
1152
      };
1153

1154
      done();
1✔
1155
    };
1156

1157
    inMemoryImage.src = imageSrc;
24✔
1158
  }
1159

1160
  // Load all images and their thumbnails
1161
  loadAllImages(props = this.props) {
6✔
1162
    const generateLoadDoneCallback = (srcType, imageSrc) => err => {
24✔
1163
      // Give up showing image on error
1164
      if (err) {
2✔
1165
        return;
1✔
1166
      }
1167

1168
      // Don't rerender if the src is not the same as when the load started
1169
      // or if the component has unmounted
1170
      if (this.props[srcType] !== imageSrc || this.didUnmount) {
1!
1171
        return;
×
1172
      }
1173

1174
      // Force rerender with the new image
1175
      this.forceUpdate();
1✔
1176
    };
1177

1178
    // Load the images
1179
    this.getSrcTypes().forEach(srcType => {
12✔
1180
      const type = srcType.name;
72✔
1181

1182
      // there is no error when we try to load it initially
1183
      if (props[type] && this.state.loadErrorStatus[type]) {
72!
1184
        this.setState(prevState => ({
×
1185
          loadErrorStatus: { ...prevState.loadErrorStatus, [type]: false },
1186
        }));
1187
      }
1188

1189
      // Load unloaded images
1190
      if (props[type] && !this.isImageLoaded(props[type])) {
72✔
1191
        this.loadImage(
24✔
1192
          type,
1193
          props[type],
1194
          generateLoadDoneCallback(type, props[type])
1195
        );
1196
      }
1197
    });
1198
  }
1199

1200
  // Request that the lightbox be closed
1201
  requestClose(event) {
1202
    // Call the parent close request
1203
    const closeLightbox = () => this.props.onCloseRequest(event);
2✔
1204

1205
    if (
2!
1206
      this.props.animationDisabled ||
4✔
1207
      (event.type === 'keydown' && !this.props.animationOnKeyInput)
1208
    ) {
1209
      // No animation
1210
      closeLightbox();
2✔
1211
      return;
2✔
1212
    }
1213

1214
    // With animation
1215
    // Start closing animation
1216
    this.setState({ isClosing: true });
×
1217

1218
    // Perform the actual closing at the end of the animation
1219
    this.setTimeout(closeLightbox, this.props.animationDuration);
×
1220
  }
1221

1222
  requestMove(direction, event) {
1223
    // Reset the zoom level on image move
1224
    const nextState = {
4✔
1225
      zoomLevel: MIN_ZOOM_LEVEL,
1226
      offsetX: 0,
1227
      offsetY: 0,
1228
    };
1229

1230
    // Enable animated states
1231
    if (
4!
1232
      !this.props.animationDisabled &&
8✔
1233
      (!this.keyPressed || this.props.animationOnKeyInput)
1234
    ) {
1235
      nextState.shouldAnimate = true;
×
1236
      this.setTimeout(
×
1237
        () => this.setState({ shouldAnimate: false }),
×
1238
        this.props.animationDuration
1239
      );
1240
    }
1241
    this.keyPressed = false;
4✔
1242

1243
    this.moveRequested = true;
4✔
1244

1245
    if (direction === 'prev') {
4✔
1246
      this.keyCounter -= 1;
2✔
1247
      this.setState(nextState);
2✔
1248
      this.props.onMovePrevRequest(event);
2✔
1249
    } else {
1250
      this.keyCounter += 1;
2✔
1251
      this.setState(nextState);
2✔
1252
      this.props.onMoveNextRequest(event);
2✔
1253
    }
1254
  }
1255

1256
  // Request to transition to the next image
1257
  requestMoveNext(event) {
1258
    this.requestMove('next', event);
2✔
1259
  }
1260

1261
  // Request to transition to the previous image
1262
  requestMovePrev(event) {
1263
    this.requestMove('prev', event);
2✔
1264
  }
1265

1266
  render() {
1267
    const {
1268
      animationDisabled,
1269
      animationDuration,
1270
      clickOutsideToClose,
1271
      discourageDownloads,
1272
      enableZoom,
1273
      imageTitle,
1274
      nextSrc,
1275
      prevSrc,
1276
      toolbarButtons,
1277
      reactModalStyle,
1278
      onAfterOpen,
1279
      imageCrossOrigin,
1280
      reactModalProps,
1281
      loader,
1282
    } = this.props;
29✔
1283
    const {
1284
      zoomLevel,
1285
      offsetX,
1286
      offsetY,
1287
      isClosing,
1288
      loadErrorStatus,
1289
    } = this.state;
29✔
1290

1291
    const boxSize = this.getLightboxRect();
29✔
1292
    let transitionStyle = {};
29✔
1293

1294
    // Transition settings for sliding animations
1295
    if (!animationDisabled && this.isAnimating()) {
29✔
1296
      transitionStyle = {
5✔
1297
        ...transitionStyle,
1298
        transition: `transform ${animationDuration}ms`,
1299
      };
1300
    }
1301

1302
    // Key endings to differentiate between images with the same src
1303
    const keyEndings = {};
29✔
1304
    this.getSrcTypes().forEach(({ name, keyEnding }) => {
29✔
1305
      keyEndings[name] = keyEnding;
174✔
1306
    });
1307

1308
    // Images to be displayed
1309
    const images = [];
29✔
1310
    const addImage = (srcType, imageClass, transforms) => {
29✔
1311
      // Ignore types that have no source defined for their full size image
1312
      if (!this.props[srcType]) {
87✔
1313
        return;
28✔
1314
      }
1315
      const bestImageInfo = this.getBestImageForType(srcType);
59✔
1316

1317
      const imageStyle = {
59✔
1318
        ...transitionStyle,
1319
        ...ReactImageLightbox.getTransform({
1320
          ...transforms,
1321
          ...bestImageInfo,
1322
        }),
1323
      };
1324

1325
      if (zoomLevel > MIN_ZOOM_LEVEL) {
59✔
1326
        imageStyle.cursor = 'move';
6✔
1327
      }
1328

1329
      // support IE 9 and 11
1330
      const hasTrueValue = object =>
59✔
1331
        Object.keys(object).some(key => object[key]);
58✔
1332

1333
      // when error on one of the loads then push custom error stuff
1334
      if (bestImageInfo === null && hasTrueValue(loadErrorStatus)) {
59✔
1335
        images.push(
15✔
1336
          <div
1337
            className={`${imageClass} ril__image ril-errored`}
1338
            style={imageStyle}
1339
            key={this.props[srcType] + keyEndings[srcType]}
1340
          >
1341
            <div className="ril__errorContainer">
1342
              {this.props.imageLoadErrorMessage}
1343
            </div>
1344
          </div>
1345
        );
1346

1347
        return;
15✔
1348
      }
1349
      if (bestImageInfo === null) {
44✔
1350
        const loadingIcon =
1351
          loader !== undefined ? (
43!
1352
            loader
1353
          ) : (
1354
            <div className="ril-loading-circle ril__loadingCircle ril__loadingContainer__icon">
1355
              {[...new Array(12)].map((_, index) => (
1356
                <div
516✔
1357
                  // eslint-disable-next-line react/no-array-index-key
1358
                  key={index}
1359
                  className="ril-loading-circle-point ril__loadingCirclePoint"
1360
                />
1361
              ))}
1362
            </div>
1363
          );
1364

1365
        // Fall back to loading icon if the thumbnail has not been loaded
1366
        images.push(
43✔
1367
          <div
1368
            className={`${imageClass} ril__image ril-not-loaded`}
1369
            style={imageStyle}
1370
            key={this.props[srcType] + keyEndings[srcType]}
1371
          >
1372
            <div className="ril__loadingContainer">{loadingIcon}</div>
1373
          </div>
1374
        );
1375

1376
        return;
43✔
1377
      }
1378

1379
      const imageSrc = bestImageInfo.src;
1✔
1380
      if (discourageDownloads) {
1!
1381
        imageStyle.backgroundImage = `url('${imageSrc}')`;
×
1382
        images.push(
×
1383
          <div
1384
            className={`${imageClass} ril__image ril__imageDiscourager`}
1385
            onDoubleClick={this.handleImageDoubleClick}
1386
            onWheel={this.handleImageMouseWheel}
1387
            style={imageStyle}
1388
            key={imageSrc + keyEndings[srcType]}
1389
          >
1390
            <div className="ril-download-blocker ril__downloadBlocker" />
1391
          </div>
1392
        );
1393
      } else {
1394
        images.push(
1✔
1395
          <img
1396
            {...(imageCrossOrigin ? { crossOrigin: imageCrossOrigin } : {})}
1!
1397
            className={`${imageClass} ril__image`}
1398
            onDoubleClick={this.handleImageDoubleClick}
1399
            onWheel={this.handleImageMouseWheel}
1400
            onDragStart={e => e.preventDefault()}
×
1401
            style={imageStyle}
1402
            src={imageSrc}
1403
            key={imageSrc + keyEndings[srcType]}
1404
            alt={
1405
              typeof imageTitle === 'string' ? imageTitle : translate('Image')
1!
1406
            }
1407
            draggable={false}
1408
          />
1409
        );
1410
      }
1411
    };
1412

1413
    const zoomMultiplier = this.getZoomMultiplier();
29✔
1414
    // Next Image (displayed on the right)
1415
    addImage('nextSrc', 'ril-image-next ril__imageNext', {
29✔
1416
      x: boxSize.width,
1417
    });
1418
    // Main Image
1419
    addImage('mainSrc', 'ril-image-current', {
29✔
1420
      x: -1 * offsetX,
1421
      y: -1 * offsetY,
1422
      zoom: zoomMultiplier,
1423
    });
1424
    // Previous Image (displayed on the left)
1425
    addImage('prevSrc', 'ril-image-prev ril__imagePrev', {
29✔
1426
      x: -1 * boxSize.width,
1427
    });
1428

1429
    const modalStyle = {
29✔
1430
      overlay: {
1431
        zIndex: 1000,
1432
        backgroundColor: 'transparent',
1433
        ...reactModalStyle.overlay, // Allow style overrides via props
1434
      },
1435
      content: {
1436
        backgroundColor: 'transparent',
1437
        overflow: 'hidden', // Needed, otherwise keyboard shortcuts scroll the page
1438
        border: 'none',
1439
        borderRadius: 0,
1440
        padding: 0,
1441
        top: 0,
1442
        left: 0,
1443
        right: 0,
1444
        bottom: 0,
1445
        ...reactModalStyle.content, // Allow style overrides via props
1446
      },
1447
    };
1448

1449
    return (
29✔
1450
      <Modal
1451
        isOpen
1452
        onRequestClose={clickOutsideToClose ? this.requestClose : undefined}
29!
1453
        onAfterOpen={() => {
1454
          // Focus on the div with key handlers
1455
          if (this.outerEl.current) {
3!
1456
            this.outerEl.current.focus();
3✔
1457
          }
1458

1459
          onAfterOpen();
3✔
1460
        }}
1461
        style={modalStyle}
1462
        contentLabel={translate('Lightbox')}
1463
        appElement={
1464
          typeof global.window !== 'undefined'
29!
1465
            ? global.window.document.body
1466
            : undefined
1467
        }
1468
        {...reactModalProps}
1469
      >
1470
        <div // eslint-disable-line jsx-a11y/no-static-element-interactions
1471
          // Floating modal with closing animations
1472
          className={`ril-outer ril__outer ril__outerAnimating ${
1473
            this.props.wrapperClassName
1474
          } ${isClosing ? 'ril-closing ril__outerClosing' : ''}`}
29✔
1475
          style={{
1476
            transition: `opacity ${animationDuration}ms`,
1477
            animationDuration: `${animationDuration}ms`,
1478
            animationDirection: isClosing ? 'normal' : 'reverse',
29✔
1479
          }}
1480
          ref={this.outerEl}
1481
          onWheel={this.handleOuterMousewheel}
1482
          onMouseMove={this.handleMouseMove}
1483
          onMouseDown={this.handleMouseDown}
1484
          onTouchStart={this.handleTouchStart}
1485
          onTouchMove={this.handleTouchMove}
1486
          tabIndex="-1" // Enables key handlers on div
1487
          onKeyDown={this.handleKeyInput}
1488
          onKeyUp={this.handleKeyInput}
1489
        >
1490
          <div // eslint-disable-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events
1491
            // Image holder
1492
            className="ril-inner ril__inner"
1493
            onClick={clickOutsideToClose ? this.closeIfClickInner : undefined}
29!
1494
          >
1495
            {images}
1496
          </div>
1497

1498
          {prevSrc && (
44✔
1499
            <button // Move to previous image button
1500
              type="button"
1501
              className="ril-prev-button ril__navButtons ril__navButtonPrev"
1502
              key="prev"
1503
              aria-label={this.props.prevLabel}
1504
              title={this.props.prevLabel}
1505
              onClick={!this.isAnimating() ? this.requestMovePrev : undefined} // Ignore clicks during animation
15!
1506
            />
1507
          )}
1508

1509
          {nextSrc && (
44✔
1510
            <button // Move to next image button
1511
              type="button"
1512
              className="ril-next-button ril__navButtons ril__navButtonNext"
1513
              key="next"
1514
              aria-label={this.props.nextLabel}
1515
              title={this.props.nextLabel}
1516
              onClick={!this.isAnimating() ? this.requestMoveNext : undefined} // Ignore clicks during animation
15!
1517
            />
1518
          )}
1519

1520
          <div // Lightbox toolbar
1521
            className="ril-toolbar ril__toolbar"
1522
          >
1523
            <ul className="ril-toolbar-left ril__toolbarSide ril__toolbarLeftSide">
1524
              <li className="ril-toolbar__item ril__toolbarItem">
1525
                <span className="ril-toolbar__item__child ril__toolbarItemChild">
1526
                  {imageTitle}
1527
                </span>
1528
              </li>
1529
            </ul>
1530

1531
            <ul className="ril-toolbar-right ril__toolbarSide ril__toolbarRightSide">
1532
              {toolbarButtons &&
31✔
1533
                toolbarButtons.map((button, i) => (
1534
                  <li
2✔
1535
                    key={`button_${i + 1}`}
1536
                    className="ril-toolbar__item ril__toolbarItem"
1537
                  >
1538
                    {button}
1539
                  </li>
1540
                ))}
1541

1542
              {enableZoom && (
54✔
1543
                <li className="ril-toolbar__item ril__toolbarItem">
1544
                  <button // Lightbox zoom in button
1545
                    type="button"
1546
                    key="zoom-in"
1547
                    aria-label={this.props.zoomInLabel}
1548
                    title={this.props.zoomInLabel}
1549
                    className={[
1550
                      'ril-zoom-in',
1551
                      'ril__toolbarItemChild',
1552
                      'ril__builtinButton',
1553
                      'ril__zoomInButton',
1554
                      ...(zoomLevel === MAX_ZOOM_LEVEL
25!
1555
                        ? ['ril__builtinButtonDisabled']
1556
                        : []),
1557
                    ].join(' ')}
1558
                    ref={this.zoomInBtn}
1559
                    disabled={
1560
                      this.isAnimating() || zoomLevel === MAX_ZOOM_LEVEL
45✔
1561
                    }
1562
                    onClick={
1563
                      !this.isAnimating() && zoomLevel !== MAX_ZOOM_LEVEL
70✔
1564
                        ? this.handleZoomInButtonClick
1565
                        : undefined
1566
                    }
1567
                  />
1568
                </li>
1569
              )}
1570

1571
              {enableZoom && (
54✔
1572
                <li className="ril-toolbar__item ril__toolbarItem">
1573
                  <button // Lightbox zoom out button
1574
                    type="button"
1575
                    key="zoom-out"
1576
                    aria-label={this.props.zoomOutLabel}
1577
                    title={this.props.zoomOutLabel}
1578
                    className={[
1579
                      'ril-zoom-out',
1580
                      'ril__toolbarItemChild',
1581
                      'ril__builtinButton',
1582
                      'ril__zoomOutButton',
1583
                      ...(zoomLevel === MIN_ZOOM_LEVEL
25✔
1584
                        ? ['ril__builtinButtonDisabled']
1585
                        : []),
1586
                    ].join(' ')}
1587
                    ref={this.zoomOutBtn}
1588
                    disabled={
1589
                      this.isAnimating() || zoomLevel === MIN_ZOOM_LEVEL
45✔
1590
                    }
1591
                    onClick={
1592
                      !this.isAnimating() && zoomLevel !== MIN_ZOOM_LEVEL
70✔
1593
                        ? this.handleZoomOutButtonClick
1594
                        : undefined
1595
                    }
1596
                  />
1597
                </li>
1598
              )}
1599

1600
              <li className="ril-toolbar__item ril__toolbarItem">
1601
                <button // Lightbox close button
1602
                  type="button"
1603
                  key="close"
1604
                  aria-label={this.props.closeLabel}
1605
                  title={this.props.closeLabel}
1606
                  className="ril-close ril-toolbar__item__child ril__toolbarItemChild ril__builtinButton ril__closeButton"
1607
                  onClick={!this.isAnimating() ? this.requestClose : undefined} // Ignore clicks during animation
29✔
1608
                />
1609
              </li>
1610
            </ul>
1611
          </div>
1612

1613
          {this.props.imageCaption && (
29!
1614
            // eslint-disable-next-line jsx-a11y/no-static-element-interactions
1615
            <div // Image caption
1616
              onWheel={this.handleCaptionMousewheel}
1617
              onMouseDown={event => event.stopPropagation()}
×
1618
              className="ril-caption ril__caption"
1619
              ref={this.caption}
1620
            >
1621
              <div className="ril-caption-content ril__captionContent">
1622
                {this.props.imageCaption}
1623
              </div>
1624
            </div>
1625
          )}
1626
        </div>
1627
      </Modal>
1628
    );
1629
  }
1630
}
1631

1632
ReactImageLightbox.propTypes = {
1✔
1633
  //-----------------------------
1634
  // Image sources
1635
  //-----------------------------
1636

1637
  // Main display image url
1638
  mainSrc: PropTypes.string.isRequired, // eslint-disable-line react/no-unused-prop-types
1639

1640
  // Previous display image url (displayed to the left)
1641
  // If left undefined, movePrev actions will not be performed, and the button not displayed
1642
  prevSrc: PropTypes.string,
1643

1644
  // Next display image url (displayed to the right)
1645
  // If left undefined, moveNext actions will not be performed, and the button not displayed
1646
  nextSrc: PropTypes.string,
1647

1648
  //-----------------------------
1649
  // Image thumbnail sources
1650
  //-----------------------------
1651

1652
  // Thumbnail image url corresponding to props.mainSrc
1653
  mainSrcThumbnail: PropTypes.string, // eslint-disable-line react/no-unused-prop-types
1654

1655
  // Thumbnail image url corresponding to props.prevSrc
1656
  prevSrcThumbnail: PropTypes.string, // eslint-disable-line react/no-unused-prop-types
1657

1658
  // Thumbnail image url corresponding to props.nextSrc
1659
  nextSrcThumbnail: PropTypes.string, // eslint-disable-line react/no-unused-prop-types
1660

1661
  //-----------------------------
1662
  // Event Handlers
1663
  //-----------------------------
1664

1665
  // Close window event
1666
  // Should change the parent state such that the lightbox is not rendered
1667
  onCloseRequest: PropTypes.func.isRequired,
1668

1669
  // Move to previous image event
1670
  // Should change the parent state such that props.prevSrc becomes props.mainSrc,
1671
  //  props.mainSrc becomes props.nextSrc, etc.
1672
  onMovePrevRequest: PropTypes.func,
1673

1674
  // Move to next image event
1675
  // Should change the parent state such that props.nextSrc becomes props.mainSrc,
1676
  //  props.mainSrc becomes props.prevSrc, etc.
1677
  onMoveNextRequest: PropTypes.func,
1678

1679
  // Called when an image fails to load
1680
  // (imageSrc: string, srcType: string, errorEvent: object): void
1681
  onImageLoadError: PropTypes.func,
1682

1683
  // Called when image successfully loads
1684
  onImageLoad: PropTypes.func,
1685

1686
  // Open window event
1687
  onAfterOpen: PropTypes.func,
1688

1689
  //-----------------------------
1690
  // Download discouragement settings
1691
  //-----------------------------
1692

1693
  // Enable download discouragement (prevents [right-click -> Save Image As...])
1694
  discourageDownloads: PropTypes.bool,
1695

1696
  //-----------------------------
1697
  // Animation settings
1698
  //-----------------------------
1699

1700
  // Disable all animation
1701
  animationDisabled: PropTypes.bool,
1702

1703
  // Disable animation on actions performed with keyboard shortcuts
1704
  animationOnKeyInput: PropTypes.bool,
1705

1706
  // Animation duration (ms)
1707
  animationDuration: PropTypes.number,
1708

1709
  //-----------------------------
1710
  // Keyboard shortcut settings
1711
  //-----------------------------
1712

1713
  // Required interval of time (ms) between key actions
1714
  // (prevents excessively fast navigation of images)
1715
  keyRepeatLimit: PropTypes.number,
1716

1717
  // Amount of time (ms) restored after each keyup
1718
  // (makes rapid key presses slightly faster than holding down the key to navigate images)
1719
  keyRepeatKeyupBonus: PropTypes.number,
1720

1721
  //-----------------------------
1722
  // Image info
1723
  //-----------------------------
1724

1725
  // Image title
1726
  imageTitle: PropTypes.node,
1727

1728
  // Image caption
1729
  imageCaption: PropTypes.node,
1730

1731
  // Optional crossOrigin attribute
1732
  imageCrossOrigin: PropTypes.string,
1733

1734
  //-----------------------------
1735
  // Lightbox style
1736
  //-----------------------------
1737

1738
  // Set z-index style, etc., for the parent react-modal (format: https://github.com/reactjs/react-modal#styles )
1739
  reactModalStyle: PropTypes.shape({}),
1740

1741
  // Padding (px) between the edge of the window and the lightbox
1742
  imagePadding: PropTypes.number,
1743

1744
  wrapperClassName: PropTypes.string,
1745

1746
  //-----------------------------
1747
  // Other
1748
  //-----------------------------
1749

1750
  // Array of custom toolbar buttons
1751
  toolbarButtons: PropTypes.arrayOf(PropTypes.node),
1752

1753
  // When true, clicks outside of the image close the lightbox
1754
  clickOutsideToClose: PropTypes.bool,
1755

1756
  // Set to false to disable zoom functionality and hide zoom buttons
1757
  enableZoom: PropTypes.bool,
1758

1759
  // Override props set on react-modal (https://github.com/reactjs/react-modal)
1760
  reactModalProps: PropTypes.shape({}),
1761

1762
  // Aria-labels
1763
  nextLabel: PropTypes.string,
1764
  prevLabel: PropTypes.string,
1765
  zoomInLabel: PropTypes.string,
1766
  zoomOutLabel: PropTypes.string,
1767
  closeLabel: PropTypes.string,
1768

1769
  imageLoadErrorMessage: PropTypes.node,
1770

1771
  // custom loader
1772
  loader: PropTypes.node,
1773
};
1774

1775
ReactImageLightbox.defaultProps = {
1✔
1776
  imageTitle: null,
1777
  imageCaption: null,
1778
  toolbarButtons: null,
1779
  reactModalProps: {},
1780
  animationDisabled: false,
1781
  animationDuration: 300,
1782
  animationOnKeyInput: false,
1783
  clickOutsideToClose: true,
1784
  closeLabel: 'Close lightbox',
1785
  discourageDownloads: false,
1786
  enableZoom: true,
1787
  imagePadding: 10,
1788
  imageCrossOrigin: null,
1789
  keyRepeatKeyupBonus: 40,
1790
  keyRepeatLimit: 180,
1791
  mainSrcThumbnail: null,
1792
  nextLabel: 'Next image',
1793
  nextSrc: null,
1794
  nextSrcThumbnail: null,
1795
  onAfterOpen: () => {},
1796
  onImageLoadError: () => {},
1797
  onImageLoad: () => {},
1798
  onMoveNextRequest: () => {},
1799
  onMovePrevRequest: () => {},
1800
  prevLabel: 'Previous image',
1801
  prevSrc: null,
1802
  prevSrcThumbnail: null,
1803
  reactModalStyle: {},
1804
  wrapperClassName: '',
1805
  zoomInLabel: 'Zoom in',
1806
  zoomOutLabel: 'Zoom out',
1807
  imageLoadErrorMessage: 'This image failed to load',
1808
  loader: undefined,
1809
};
1810

1811
export default ReactImageLightbox;
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc