DrawioModal.tsx 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. import React, { type JSX, useCallback, useEffect, useMemo } from 'react';
  2. import { Lang } from '@growi/core';
  3. import { useCodeMirrorEditorIsolated } from '@growi/editor/dist/client/stores/codemirror-editor';
  4. import {
  5. useDrawioModalForEditorActions,
  6. useDrawioModalForEditorStatus,
  7. } from '@growi/editor/dist/states/modal/drawio-for-editor';
  8. import { LoadingSpinner } from '@growi/ui/dist/components';
  9. import { Modal, ModalBody } from 'reactstrap';
  10. import {
  11. getMarkdownDrawioMxfile,
  12. replaceFocusedDrawioWithEditor,
  13. } from '~/client/components/PageEditor/markdown-drawio-util-for-editor';
  14. import { useRendererConfig } from '~/states/server-configurations';
  15. import {
  16. useDrawioModalActions,
  17. useDrawioModalStatus,
  18. } from '~/states/ui/modal/drawio';
  19. import { useSWRxPersonalSettings } from '~/stores/personal-settings';
  20. import loggerFactory from '~/utils/logger';
  21. import {
  22. DrawioCommunicationHelper,
  23. type DrawioConfig,
  24. } from './DrawioCommunicationHelper';
  25. const logger = loggerFactory('growi:components:DrawioModal');
  26. // https://docs.google.com/spreadsheets/d/1FoYdyEraEQuWofzbYCDPKN7EdKgS_2ZrsDrOA8scgwQ
  27. const DIAGRAMS_NET_LANG_MAP = {
  28. en_US: 'en',
  29. ja_JP: 'ja',
  30. zh_CN: 'zh',
  31. fr_FR: 'fr',
  32. };
  33. export const getDiagramsNetLangCode = (lang: Lang): string => {
  34. return DIAGRAMS_NET_LANG_MAP[lang];
  35. };
  36. const headerColor = '#334455';
  37. const fontFamily =
  38. "-apple-system, BlinkMacSystemFont, 'Hiragino Kaku Gothic ProN', Meiryo, sans-serif";
  39. const drawioConfig: DrawioConfig = {
  40. css: `
  41. .geMenubarContainer { background-color: ${headerColor} !important; }
  42. .geMenubar { background-color: ${headerColor} !important; }
  43. .geEditor { font-family: ${fontFamily} !important; }
  44. html td.mxPopupMenuItem {
  45. font-family: ${fontFamily} !important;
  46. font-size: 8pt !important;
  47. }
  48. `,
  49. customFonts: ['Charter'],
  50. compressXml: true,
  51. };
  52. const DrawioModalSubstance = (): JSX.Element => {
  53. const { drawioUri } = useRendererConfig();
  54. const { data: personalSettingsInfo } = useSWRxPersonalSettings({
  55. // make immutable
  56. revalidateIfStale: false,
  57. revalidateOnFocus: false,
  58. revalidateOnReconnect: false,
  59. });
  60. const drawioModalData = useDrawioModalStatus();
  61. const { close: closeDrawioModal } = useDrawioModalActions();
  62. const drawioModalDataInEditor = useDrawioModalForEditorStatus();
  63. const { close: closeDrawioModalInEditor } = useDrawioModalForEditorActions();
  64. const editorKey = drawioModalDataInEditor?.editorKey ?? null;
  65. const { data: codeMirrorEditor } = useCodeMirrorEditorIsolated(editorKey);
  66. const editor = codeMirrorEditor?.view;
  67. const isOpenedInEditor =
  68. (drawioModalDataInEditor?.isOpened ?? false) && editor != null;
  69. const isOpened = drawioModalData?.isOpened ?? false;
  70. // Memoize URI with parameters calculation
  71. const drawioUriWithParams = useMemo(() => {
  72. if (drawioUri === '') {
  73. return undefined;
  74. }
  75. let url;
  76. try {
  77. url = new URL(drawioUri);
  78. } catch (err) {
  79. logger.debug(err);
  80. return undefined;
  81. }
  82. // refs: https://desk.draw.io/support/solutions/articles/16000042546-what-url-parameters-are-supported-
  83. url.searchParams.append('spin', '1');
  84. url.searchParams.append('embed', '1');
  85. url.searchParams.append(
  86. 'lang',
  87. getDiagramsNetLangCode(personalSettingsInfo?.lang ?? Lang.en_US),
  88. );
  89. url.searchParams.append('ui', 'atlas');
  90. url.searchParams.append('configure', '1');
  91. return url;
  92. }, [drawioUri, personalSettingsInfo?.lang]);
  93. // Memoize communication helper with inline handlers to avoid dependency issues
  94. const drawioCommunicationHelper = useMemo(() => {
  95. if (drawioUri === '') {
  96. return undefined;
  97. }
  98. const saveHandler =
  99. editor != null
  100. ? (drawioMxFile: string) =>
  101. replaceFocusedDrawioWithEditor(editor, drawioMxFile)
  102. : drawioModalData?.onSave;
  103. const closeHandler = isOpened ? closeDrawioModal : closeDrawioModalInEditor;
  104. return new DrawioCommunicationHelper(drawioUri, drawioConfig, {
  105. onClose: closeHandler,
  106. onSave: saveHandler,
  107. });
  108. }, [
  109. drawioUri,
  110. editor,
  111. drawioModalData?.onSave,
  112. isOpened,
  113. closeDrawioModal,
  114. closeDrawioModalInEditor,
  115. ]);
  116. const receiveMessageHandler = useCallback(
  117. (event: MessageEvent) => {
  118. if (drawioModalData == null || drawioCommunicationHelper == null) {
  119. return;
  120. }
  121. const drawioMxFile =
  122. editor != null
  123. ? getMarkdownDrawioMxfile(editor)
  124. : drawioModalData.drawioMxFile;
  125. drawioCommunicationHelper.onReceiveMessage(event, drawioMxFile ?? null);
  126. },
  127. [drawioCommunicationHelper, drawioModalData, editor],
  128. );
  129. // Memoize toggle handler
  130. const toggleHandler = useCallback(() => {
  131. if (isOpened) {
  132. closeDrawioModal();
  133. } else {
  134. closeDrawioModalInEditor();
  135. }
  136. }, [isOpened, closeDrawioModal, closeDrawioModalInEditor]);
  137. useEffect(() => {
  138. if (isOpened || isOpenedInEditor) {
  139. window.addEventListener('message', receiveMessageHandler);
  140. } else {
  141. window.removeEventListener('message', receiveMessageHandler);
  142. }
  143. // clean up
  144. return () => {
  145. window.removeEventListener('message', receiveMessageHandler);
  146. };
  147. }, [isOpened, isOpenedInEditor, receiveMessageHandler]);
  148. return (
  149. <Modal
  150. isOpen={isOpened || isOpenedInEditor}
  151. toggle={toggleHandler}
  152. backdrop="static"
  153. className="drawio-modal grw-body-only-modal-expanded"
  154. size="xl"
  155. keyboard={false}
  156. >
  157. <ModalBody className="p-0">
  158. {/* Loading spinner */}
  159. <div className="w-100 h-100 position-absolute d-flex">
  160. <div className="mx-auto my-auto">
  161. <LoadingSpinner className="mx-auto text-muted fs-2" />
  162. </div>
  163. </div>
  164. {/* iframe */}
  165. {drawioUriWithParams != null && (
  166. <div className="w-100 h-100 position-absolute d-flex">
  167. {(isOpened || isOpenedInEditor) && (
  168. <iframe
  169. src={drawioUriWithParams.href}
  170. className="border-0 flex-grow-1"
  171. ></iframe>
  172. )}
  173. </div>
  174. )}
  175. </ModalBody>
  176. </Modal>
  177. );
  178. };
  179. export const DrawioModal = (): JSX.Element => {
  180. return <DrawioModalSubstance />;
  181. };