Source: document-library-preview-document/src/main/resources/META-INF/resources/preview/js/DocumentPreviewer.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 ClayLoadingIndicator from '@clayui/loading-indicator';
  17. import {useIsMounted} from '@liferay/frontend-js-react-web';
  18. import {debounce} from 'frontend-js-web';
  19. import imagePromise from 'image-promise';
  20. import PropTypes from 'prop-types';
  21. import React, {useEffect, useRef, useState} from 'react';
  22. const KEY_CODE_ENTER = 13;
  23. const KEY_CODE_ESC = 27;
  24. /**
  25. * Valid list of keycodes
  26. * Includes backspace, tab, arrows, delete and numbers
  27. * @type {Array<number>}
  28. */
  29. const VALID_KEY_CODES = [
  30. 8,
  31. 9,
  32. 37,
  33. 38,
  34. 39,
  35. 40,
  36. 46,
  37. 48,
  38. 49,
  39. 50,
  40. 51,
  41. 52,
  42. 53,
  43. 54,
  44. 55,
  45. 56,
  46. 57,
  47. ];
  48. /**
  49. * Milisecons between goToPage calls
  50. * @type {number}
  51. */
  52. const WAIT_BETWEEN_GO_TO_PAGE = 250;
  53. /**
  54. * Component that creates a pdf preview
  55. * @review
  56. */
  57. const DocumentPreviewer = ({baseImageURL, initialPage, totalPages}) => {
  58. const [currentPage, setCurrentPage] = useState(initialPage);
  59. const [currentPageLoading, setCurrentPageLoading] = useState(false);
  60. const [expanded, setExpanded] = useState(false);
  61. const [loadedPages] = useState({
  62. [currentPage]: {
  63. loaded: true,
  64. pagePromise: Promise.resolve(),
  65. },
  66. });
  67. const [nextPageDisabled, setNextPageDisabled] = useState(
  68. currentPage === totalPages
  69. );
  70. const [previousPageDisabled, setPreviousPageDisabled] = useState(
  71. currentPage === 1
  72. );
  73. const [showPageInput, setShowPageInput] = useState(false);
  74. const imageContainer = useRef();
  75. const pageInput = useRef();
  76. const showPageInputButton = useRef();
  77. const isMounted = useIsMounted();
  78. if (showPageInput) {
  79. setTimeout(() => {
  80. if (isMounted()) {
  81. pageInput.current.focus();
  82. }
  83. }, 100);
  84. }
  85. const loadPage = (page) => {
  86. let pagePromise = loadedPages[page] && loadedPages[page].pagePromise;
  87. if (!pagePromise) {
  88. pagePromise = imagePromise(`${baseImageURL}${page}`).then(() => {
  89. loadedPages[page].loaded = true;
  90. });
  91. loadedPages[page] = {
  92. loaded: false,
  93. pagePromise,
  94. };
  95. }
  96. return pagePromise;
  97. };
  98. const loadAdjacentPages = (page, adjacentPageCount = 2) => {
  99. for (let i = 1; i <= adjacentPageCount; i++) {
  100. if (page + i <= totalPages) {
  101. loadPage(page + i);
  102. }
  103. if (page - i > 1) {
  104. loadPage(page - i);
  105. }
  106. }
  107. };
  108. const loadCurrentPage = debounce((page) => {
  109. loadPage(page)
  110. .then(() => {
  111. loadAdjacentPages(page);
  112. setCurrentPageLoading(false);
  113. })
  114. .catch(() => {
  115. setCurrentPageLoading(false);
  116. });
  117. }, WAIT_BETWEEN_GO_TO_PAGE);
  118. const goToPage = (page) => {
  119. setNextPageDisabled(page === totalPages);
  120. setPreviousPageDisabled(page === 1);
  121. if (!loadedPages[page] || !loadedPages[page].loaded) {
  122. setCurrentPageLoading(true);
  123. loadCurrentPage(page);
  124. }
  125. imageContainer.current.scrollTop = 0;
  126. setCurrentPage(page);
  127. };
  128. const processPageInput = (value) => {
  129. let pageNumber = Number.parseInt(value, 10);
  130. pageNumber = pageNumber
  131. ? Math.min(Math.max(1, pageNumber), totalPages)
  132. : currentPage;
  133. goToPage(pageNumber);
  134. };
  135. const hidePageInput = (returnFocus = true) => {
  136. setShowPageInput(false);
  137. if (returnFocus) {
  138. setTimeout(() => {
  139. if (isMounted()) {
  140. showPageInputButton.current.focus();
  141. }
  142. }, 100);
  143. }
  144. };
  145. const handleBlurPageInput = (event) => {
  146. processPageInput(event.currentTarget.value);
  147. hidePageInput(false);
  148. };
  149. const handleKeyDownPageInput = (event) => {
  150. const code = event.keyCode || event.charCode;
  151. if (code === KEY_CODE_ENTER) {
  152. processPageInput(event.currentTarget.value);
  153. hidePageInput();
  154. }
  155. else if (code === KEY_CODE_ESC) {
  156. hidePageInput();
  157. }
  158. else if (VALID_KEY_CODES.indexOf(code) === -1) {
  159. event.preventDefault();
  160. }
  161. };
  162. useEffect(() => {
  163. loadAdjacentPages(initialPage);
  164. // eslint-disable-next-line react-hooks/exhaustive-deps
  165. }, []);
  166. return (
  167. <div className="preview-file">
  168. <div
  169. className="preview-file-container preview-file-max-height"
  170. ref={imageContainer}
  171. >
  172. {currentPageLoading ? (
  173. <ClayLoadingIndicator />
  174. ) : (
  175. <img
  176. className={`preview-file-document ${
  177. !expanded && 'preview-file-document-fit'
  178. }`}
  179. src={`${baseImageURL}${currentPage}`}
  180. />
  181. )}
  182. </div>
  183. <div className="preview-toolbar-container">
  184. <ClayButton.Group className="floating-bar">
  185. <ClayButton.Group>
  186. <ClayButton
  187. className="btn-floating-bar btn-floating-bar-text"
  188. onClick={() => {
  189. setShowPageInput(true);
  190. }}
  191. ref={showPageInputButton}
  192. title={
  193. totalPages > 1 &&
  194. Liferay.Language.get('click-to-jump-to-a-page')
  195. }
  196. >
  197. {`${Liferay.Language.get(
  198. 'page'
  199. )} ${currentPage} / ${totalPages}`}
  200. </ClayButton>
  201. {showPageInput && (
  202. <div className="floating-bar-input-wrapper">
  203. <input
  204. className="floating-bar-input form-control form-control-sm"
  205. max={totalPages}
  206. min="1"
  207. onBlur={handleBlurPageInput}
  208. onKeyDown={handleKeyDownPageInput}
  209. placeholder={Liferay.Language.get(
  210. 'page-...'
  211. )}
  212. ref={pageInput}
  213. type="number"
  214. />
  215. </div>
  216. )}
  217. </ClayButton.Group>
  218. <ClayButton
  219. className="btn-floating-bar"
  220. disabled={previousPageDisabled}
  221. monospaced
  222. onClick={() => {
  223. goToPage(currentPage - 1);
  224. }}
  225. title={Liferay.Language.get('page-above')}
  226. >
  227. <ClayIcon symbol="caret-top" />
  228. </ClayButton>
  229. <ClayButton
  230. className="btn-floating-bar"
  231. disabled={nextPageDisabled}
  232. monospaced
  233. onClick={() => {
  234. goToPage(currentPage + 1);
  235. }}
  236. title={Liferay.Language.get('page-below')}
  237. >
  238. <ClayIcon symbol="caret-bottom" />
  239. </ClayButton>
  240. <div className="separator-floating-bar"></div>
  241. <ClayButton
  242. className="btn-floating-bar"
  243. monospaced
  244. onClick={() => {
  245. setExpanded(!expanded);
  246. }}
  247. title={
  248. expanded
  249. ? Liferay.Language.get('zoom-to-fit')
  250. : Liferay.Language.get('expand')
  251. }
  252. >
  253. <ClayIcon
  254. symbol={expanded ? 'autosize' : 'full-size'}
  255. />
  256. </ClayButton>
  257. </ClayButton.Group>
  258. </div>
  259. </div>
  260. );
  261. };
  262. DocumentPreviewer.propTypes = {
  263. baseImageURL: PropTypes.string,
  264. initialPage: PropTypes.number,
  265. totalPages: PropTypes.number,
  266. };
  267. export default DocumentPreviewer;