DrawioModal.tsx 6.0 KB

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