DrawioViewer.tsx 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. import {
  2. type JSX,
  3. memo,
  4. type ReactNode,
  5. useCallback,
  6. useEffect,
  7. useMemo,
  8. useRef,
  9. useState,
  10. } from 'react';
  11. import { debounce } from 'throttle-debounce';
  12. import type { IGraphViewerGlobal } from '..';
  13. import { generateMxgraphData } from '../utils/embed';
  14. import { isGraphViewerGlobal } from '../utils/global';
  15. import styles from './DrawioViewer.module.scss';
  16. declare global {
  17. // eslint-disable-next-line vars-on-top, no-var
  18. var GraphViewer: IGraphViewerGlobal;
  19. }
  20. export type DrawioViewerProps = {
  21. isDarkMode: 'true' | 'false';
  22. diagramIndex: number;
  23. bol: number;
  24. eol: number;
  25. children?: ReactNode;
  26. onRenderingStart?: () => void;
  27. onRenderingUpdated?: (mxfile: string | null) => void;
  28. };
  29. export type DrawioEditByViewerProps = {
  30. bol: number;
  31. eol: number;
  32. drawioMxFile: string;
  33. };
  34. export const DrawioViewer = memo((props: DrawioViewerProps): JSX.Element => {
  35. const {
  36. isDarkMode,
  37. diagramIndex,
  38. bol,
  39. eol,
  40. children,
  41. onRenderingStart,
  42. onRenderingUpdated,
  43. } = props;
  44. const drawioContainerRef = useRef<HTMLDivElement>(null);
  45. const [error, setError] = useState<Error>();
  46. const renderDrawio = useCallback(() => {
  47. if (drawioContainerRef.current == null) {
  48. return;
  49. }
  50. if (!('GraphViewer' in window && isGraphViewerGlobal(GraphViewer))) {
  51. // Do nothing if loading has not been terminated.
  52. // Alternatively, GraphViewer.processElements() will be called in Script.onLoad.
  53. // see DrawioViewerScript.tsx
  54. return;
  55. }
  56. const mxgraphs = drawioContainerRef.current.getElementsByClassName(
  57. 'mxgraph',
  58. ) as HTMLCollectionOf<HTMLElement>;
  59. if (mxgraphs.length > 0) {
  60. // This component should have only one '.mxgraph' element
  61. const div = mxgraphs[0];
  62. if (div != null) {
  63. div.innerHTML = '';
  64. div.style.width = '';
  65. div.style.height = '';
  66. // render diagram with createViewerForElement
  67. try {
  68. GraphViewer.useResizeSensor = false;
  69. GraphViewer.prototype.checkVisibleState = false;
  70. GraphViewer.prototype.lightboxZIndex = 1055; // set $zindex-modal
  71. GraphViewer.prototype.toolbarZIndex = 1055; // set $zindex-modal
  72. GraphViewer.createViewerForElement(div);
  73. } catch (err) {
  74. setError(err);
  75. }
  76. }
  77. }
  78. }, []);
  79. const renderDrawioWithDebounce = useMemo(
  80. () => debounce(200, renderDrawio),
  81. [renderDrawio],
  82. );
  83. const mxgraphHtml = useMemo(() => {
  84. setError(undefined);
  85. if (children == null) {
  86. return '';
  87. }
  88. const code = Array.isArray(children)
  89. ? children
  90. .filter((elem) => typeof elem === 'string') // omit non-string elements (e.g. br element generated by line-breaks option)
  91. .join('')
  92. : children.toString();
  93. let mxgraphData: string | undefined;
  94. try {
  95. mxgraphData = generateMxgraphData(code, isDarkMode === 'true');
  96. } catch (err) {
  97. setError(err);
  98. }
  99. return `<div class="mxgraph" data-mxgraph="${mxgraphData}"></div>`;
  100. }, [children, isDarkMode]);
  101. useEffect(() => {
  102. if (mxgraphHtml.length > 0) {
  103. onRenderingStart?.();
  104. renderDrawioWithDebounce();
  105. }
  106. }, [mxgraphHtml, onRenderingStart, renderDrawioWithDebounce]);
  107. useEffect(() => {
  108. if (error != null) {
  109. onRenderingUpdated?.(null);
  110. }
  111. }, [error, onRenderingUpdated]);
  112. // **************** detect data-mxgraph has rendered ****************
  113. useEffect(() => {
  114. const container = drawioContainerRef.current;
  115. if (container == null) return;
  116. const observerCallback = (mutationRecords: MutationRecord[]) => {
  117. for (const record of mutationRecords) {
  118. const target = record.target as HTMLElement;
  119. const mxgraphData = target.dataset.mxgraph;
  120. if (mxgraphData != null) {
  121. const mxgraph = JSON.parse(mxgraphData);
  122. onRenderingUpdated?.(mxgraph.xml);
  123. }
  124. }
  125. };
  126. const observer = new MutationObserver(observerCallback);
  127. observer.observe(container, { childList: true, subtree: true });
  128. return () => {
  129. observer.disconnect();
  130. };
  131. }, [onRenderingUpdated]);
  132. // ******************************* end *******************************
  133. // ******************* detect container is resized *******************
  134. useEffect(() => {
  135. if (drawioContainerRef.current == null) {
  136. return;
  137. }
  138. const observer = new ResizeObserver((entries) => {
  139. for (const _entry of entries) {
  140. // setElementWidth(entry.contentRect.width);
  141. onRenderingStart?.();
  142. renderDrawioWithDebounce();
  143. }
  144. });
  145. observer.observe(drawioContainerRef.current);
  146. return () => {
  147. observer.disconnect();
  148. };
  149. }, [onRenderingStart, renderDrawioWithDebounce]);
  150. // ******************************* end *******************************
  151. return (
  152. <div
  153. key={`drawio-viewer-${diagramIndex}`}
  154. ref={drawioContainerRef}
  155. className={`drawio-viewer ${styles['drawio-viewer']} p-2`}
  156. data-begin-line-number-of-markdown={bol}
  157. data-end-line-number-of-markdown={eol}
  158. >
  159. {/* show error */}
  160. {error != null && (
  161. <span className="text-muted">
  162. <span className="material-symbols-outlined me-1">error</span>
  163. {error.name && <strong>{error.name}: </strong>}
  164. {error.message}
  165. </span>
  166. )}
  167. {error == null && (
  168. // biome-ignore lint/security/noDangerouslySetInnerHtml: ignore
  169. <div dangerouslySetInnerHTML={{ __html: mxgraphHtml }} />
  170. )}
  171. </div>
  172. );
  173. });
  174. DrawioViewer.displayName = 'DrawioViewer';