DrawioViewer.tsx 5.3 KB

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