ExtractedAttachments.tsx 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. import React, { useState, useCallback } from 'react';
  2. import { IAttachmentHasId } from '@growi/core';
  3. import { Property } from 'csstype';
  4. import Carousel, { Modal, ModalGateway } from 'react-images';
  5. import { RefsContext } from './util/refs-context';
  6. type Props = {
  7. attachments: IAttachmentHasId[],
  8. refsContext: RefsContext,
  9. };
  10. /**
  11. * 1. when 'fileFormat' is image, render Attachment as an image
  12. * 2. when 'fileFormat' is not image, render Attachment as an Attachment component
  13. */
  14. export const ExtractedAttachments = React.memo(({
  15. attachments,
  16. refsContext,
  17. }: Props): JSX.Element => {
  18. const [showCarousel, setShowCarousel] = useState(false);
  19. const [currentIndex, setCurrentIndex] = useState<number | null>(null);
  20. const imageClickedHandler = useCallback((index: number) => {
  21. setShowCarousel(true);
  22. setCurrentIndex(index);
  23. }, []);
  24. const getAttachmentsFilteredByFormat = useCallback(() => {
  25. return attachments
  26. .filter(attachment => attachment.fileFormat.startsWith('image/'));
  27. }, []);
  28. const getClassesAndStylesForNonGrid = useCallback(() => {
  29. const { options } = refsContext;
  30. const width = options?.width;
  31. const height = options?.height;
  32. const maxWidth = options?.maxWidth;
  33. const maxHeight = options?.maxHeight;
  34. const display = options?.display || 'block';
  35. const containerStyles = {
  36. width, height, maxWidth, maxHeight, display,
  37. };
  38. const imageClasses = [];
  39. const imageStyles = {
  40. width, height, maxWidth, maxHeight,
  41. };
  42. return {
  43. containerStyles,
  44. imageClasses,
  45. imageStyles,
  46. };
  47. }, []);
  48. const getClassesAndStylesForGrid = useCallback(() => {
  49. const { options } = refsContext;
  50. const maxWidth = options?.maxWidth;
  51. const maxHeight = options?.maxHeight;
  52. const containerStyles = {
  53. width: refsContext.getOptGridWidth(),
  54. height: refsContext.getOptGridHeight(),
  55. maxWidth,
  56. maxHeight,
  57. };
  58. const imageClasses = ['w-100', 'h-100'];
  59. const imageStyles = {
  60. objectFit: 'cover' as Property.ObjectFit,
  61. maxWidth,
  62. maxHeight,
  63. };
  64. return {
  65. containerStyles,
  66. imageClasses,
  67. imageStyles,
  68. };
  69. }, []);
  70. /**
  71. * wrapper method for getClassesAndStylesForGrid/getClassesAndStylesForNonGrid
  72. */
  73. const getClassesAndStyles = useCallback(() => {
  74. const { options } = refsContext;
  75. return (options?.grid != null)
  76. ? getClassesAndStylesForGrid()
  77. : getClassesAndStylesForNonGrid();
  78. }, []);
  79. const renderExtractedImage = useCallback((attachment: IAttachmentHasId, index: number) => {
  80. const { options } = refsContext;
  81. // determine alt
  82. let alt = refsContext.isSingle ? options?.alt : undefined; // use only when single mode
  83. alt = alt || attachment.originalName; // use 'originalName' if options.alt is not specified
  84. // get styles
  85. const {
  86. containerStyles, imageClasses, imageStyles,
  87. } = getClassesAndStyles();
  88. // carousel settings
  89. let onClick;
  90. if (options?.noCarousel == null) {
  91. // pointer cursor
  92. Object.assign(containerStyles, { cursor: 'pointer' });
  93. // set click handler
  94. onClick = () => {
  95. imageClickedHandler(index);
  96. };
  97. }
  98. return (
  99. <div key={attachment._id} style={containerStyles} onClick={onClick}>
  100. <img src={attachment.filePathProxied} alt={alt} className={imageClasses.join(' ')} style={imageStyles} />
  101. </div>
  102. );
  103. }, []);
  104. const renderCarousel = useCallback(() => {
  105. const { options } = refsContext;
  106. const withCarousel = options?.noCarousel == null;
  107. const images = getAttachmentsFilteredByFormat()
  108. .map((attachment) => {
  109. return { src: attachment.filePathProxied };
  110. });
  111. // overwrite react-images modal styles
  112. const zIndex = 1030; // > grw-navbar
  113. const modalStyles = {
  114. blanket: (styleObj) => {
  115. return Object.assign(styleObj, { zIndex });
  116. },
  117. positioner: (styleObj) => {
  118. return Object.assign(styleObj, { zIndex });
  119. },
  120. };
  121. return (
  122. <ModalGateway>
  123. { withCarousel && showCarousel && (
  124. <Modal styles={modalStyles} onClose={() => { setShowCarousel(false) }}>
  125. <Carousel views={images} currentIndex={currentIndex} />
  126. </Modal>
  127. ) }
  128. </ModalGateway>
  129. );
  130. }, []);
  131. const { options } = refsContext;
  132. const grid = options?.grid;
  133. const gridGap = options?.gridGap;
  134. const styles = {};
  135. // Grid mode
  136. if (grid != null) {
  137. const gridTemplateColumns = (refsContext.isOptGridColumnEnabled())
  138. ? `repeat(${refsContext.getOptGridColumnsNum()}, 1fr)`
  139. : `repeat(auto-fill, ${refsContext.getOptGridWidth()})`;
  140. Object.assign(styles, {
  141. display: 'grid',
  142. gridTemplateColumns,
  143. gridAutoRows: '1fr',
  144. gridGap,
  145. });
  146. }
  147. const contents = getAttachmentsFilteredByFormat()
  148. .map((attachment, index) => renderExtractedImage(attachment, index));
  149. return (
  150. <React.Fragment>
  151. <div style={styles}>
  152. {contents}
  153. </div>
  154. { renderCarousel() }
  155. </React.Fragment>
  156. );
  157. });