Source: document-library-preview-image/src/main/resources/META-INF/resources/preview/js/ImagePreviewer.es.js

  1. /**
  2. * Copyright (c) 2000-present Liferay, Inc. All rights reserved.
  3. *
  4. * This library is free software; you can redistribute it and/or modify it under
  5. * the terms of the GNU Lesser General Public License as published by the Free
  6. * Software Foundation; either version 2.1 of the License, or (at your option)
  7. * any later version.
  8. *
  9. * This library is distributed in the hope that it will be useful, but WITHOUT
  10. * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
  11. * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
  12. * details.
  13. */
  14. import ClayButton from '@clayui/button';
  15. import ClayIcon from '@clayui/icon';
  16. import {useEventListener, useIsMounted} from '@liferay/frontend-js-react-web';
  17. import {debounce} from 'frontend-js-web';
  18. import PropTypes from 'prop-types';
  19. import React, {useLayoutEffect, useRef, useState} from 'react';
  20. /**
  21. * Zoom ratio limit that fire the autocenter
  22. * @type {number}
  23. */
  24. const MIN_ZOOM_RATIO_AUTOCENTER = 3;
  25. /**
  26. * Available zoom sizes
  27. * @type {Array<number>}
  28. */
  29. const ZOOM_LEVELS = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1];
  30. /**
  31. * Available reversed zoom sizes
  32. * @type {Array<number>}
  33. */
  34. const ZOOM_LEVELS_REVERSED = ZOOM_LEVELS.slice().reverse();
  35. /**
  36. * Component that create an image preview to allow zoom
  37. * @review
  38. */
  39. const ImagePreviewer = ({alt, imageURL}) => {
  40. const [currentZoom, setCurrentZoom] = useState(1);
  41. const [imageHeight, setImageHeight] = useState(null);
  42. const [imageWidth, setImageWidth] = useState(null);
  43. const [imageMargin, setImageMargin] = useState(null);
  44. const [zoomInDisabled, setZoomInDisabled] = useState(true);
  45. const [zoomOutDisabled, setZoomOutDisabled] = useState(false);
  46. const [zoomRatio, setZoomRatio] = useState(false);
  47. const image = useRef();
  48. const imageContainer = useRef();
  49. const isMounted = useIsMounted();
  50. const updateToolbar = (zoom) => {
  51. setCurrentZoom(zoom);
  52. setZoomInDisabled(ZOOM_LEVELS_REVERSED[0] === zoom);
  53. setZoomOutDisabled(ZOOM_LEVELS[0] >= zoom);
  54. };
  55. const applyZoom = (zoom) => {
  56. const imageElement = image.current;
  57. setImageHeight(imageElement.naturalHeight * zoom);
  58. setImageWidth(imageElement.naturalWidth * zoom);
  59. setZoomRatio(zoom / currentZoom);
  60. updateToolbar(zoom);
  61. };
  62. const getFittingZoom = () => {
  63. const imageElement = image.current;
  64. return imageElement.width / imageElement.naturalWidth;
  65. };
  66. const getImageStyles = () => {
  67. const imageStyles = {};
  68. if (imageHeight && imageWidth) {
  69. imageStyles.height = imageHeight;
  70. imageStyles.maxHeight = imageHeight;
  71. imageStyles.maxWidth = imageWidth;
  72. imageStyles.width = imageWidth;
  73. }
  74. if (imageMargin) {
  75. imageStyles.margin = imageMargin;
  76. }
  77. return imageStyles;
  78. };
  79. const handleImageLoad = () => {
  80. updateToolbar(getFittingZoom());
  81. };
  82. const handlePercentButtonClick = () => {
  83. if (currentZoom === 1) {
  84. setImageHeight(null);
  85. setImageWidth(null);
  86. }
  87. else {
  88. applyZoom(1);
  89. }
  90. };
  91. const handleWindowResize = debounce(() => {
  92. if (isMounted() && !image.current.style.width) {
  93. updateToolbar(getFittingZoom());
  94. }
  95. }, 250);
  96. useEventListener('resize', handleWindowResize, false, window);
  97. useLayoutEffect(() => {
  98. const imageContainerElement = imageContainer.current;
  99. setImageMargin(
  100. `${imageHeight > imageContainerElement.clientHeight ? 0 : 'auto'} ${
  101. imageWidth > imageContainerElement.clientWidth ? 0 : 'auto'
  102. }`
  103. );
  104. if (
  105. zoomRatio &&
  106. (imageContainerElement.clientWidth < image.current.naturalWidth ||
  107. imageContainerElement.clientHeight <
  108. image.current.naturalHeight)
  109. ) {
  110. let scrollLeft;
  111. let scrollTop;
  112. if (zoomRatio < MIN_ZOOM_RATIO_AUTOCENTER) {
  113. scrollLeft =
  114. (imageContainerElement.clientWidth * (zoomRatio - 1)) / 2 +
  115. imageContainerElement.scrollLeft * zoomRatio;
  116. scrollTop =
  117. (imageContainerElement.clientHeight * (zoomRatio - 1)) / 2 +
  118. imageContainerElement.scrollTop * zoomRatio;
  119. }
  120. else {
  121. scrollTop =
  122. (imageHeight - imageContainerElement.clientHeight) / 2;
  123. scrollLeft =
  124. (imageWidth - imageContainerElement.clientWidth) / 2;
  125. }
  126. imageContainerElement.scrollLeft = scrollLeft;
  127. imageContainerElement.scrollTop = scrollTop;
  128. setZoomRatio(null);
  129. }
  130. if (!image.current.style.width) {
  131. updateToolbar(getFittingZoom());
  132. }
  133. }, [imageHeight, imageWidth, zoomRatio, imageMargin]);
  134. return (
  135. <div className="preview-file">
  136. <div
  137. className="preview-file-container preview-file-max-height"
  138. ref={imageContainer}
  139. >
  140. <img
  141. alt={alt}
  142. className="preview-file-image"
  143. onLoad={handleImageLoad}
  144. ref={image}
  145. src={imageURL}
  146. style={getImageStyles()}
  147. />
  148. </div>
  149. <div className="preview-toolbar-container">
  150. <ClayButton.Group className="floating-bar">
  151. <ClayButton
  152. className="btn-floating-bar"
  153. disabled={zoomOutDisabled}
  154. displayType={null}
  155. monospaced
  156. onClick={() => {
  157. applyZoom(
  158. ZOOM_LEVELS_REVERSED.find(
  159. (zoom) => zoom < currentZoom
  160. )
  161. );
  162. }}
  163. title={Liferay.Language.get('zoom-out')}
  164. >
  165. <ClayIcon symbol="hr" />
  166. </ClayButton>
  167. <ClayButton
  168. className="btn-floating-bar btn-floating-bar-text"
  169. displayType={null}
  170. onClick={handlePercentButtonClick}
  171. title={
  172. currentZoom === 1
  173. ? Liferay.Language.get('zoom-to-fit')
  174. : Liferay.Language.get('real-size')
  175. }
  176. >
  177. <span className="preview-toolbar-label-percent">
  178. {Math.round((currentZoom || 0) * 100)}%
  179. </span>
  180. </ClayButton>
  181. <ClayButton
  182. className="btn-floating-bar"
  183. disabled={zoomInDisabled}
  184. displayType={null}
  185. monospaced
  186. onClick={() => {
  187. applyZoom(
  188. ZOOM_LEVELS.find((zoom) => zoom > currentZoom)
  189. );
  190. }}
  191. title={Liferay.Language.get('zoom-in')}
  192. >
  193. <ClayIcon symbol="plus" />
  194. </ClayButton>
  195. </ClayButton.Group>
  196. </div>
  197. </div>
  198. );
  199. };
  200. ImagePreviewer.propTypes = {
  201. imageURL: PropTypes.string,
  202. };
  203. export default ImagePreviewer;