Drawio.tsx 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. import React, {
  2. useCallback, useEffect, useMemo, useRef, useState,
  3. } from 'react';
  4. import EventEmitter from 'events';
  5. import { useTranslation } from 'next-i18next';
  6. import { debounce } from 'throttle-debounce';
  7. import { CustomWindow } from '~/interfaces/global';
  8. import { IGraphViewer, isGraphViewer } from '~/interfaces/graph-viewer';
  9. import NotAvailableForGuest from './NotAvailableForGuest';
  10. type Props = {
  11. GraphViewer: IGraphViewer,
  12. drawioContent: string,
  13. rangeLineNumberOfMarkdown: { beginLineNumber: number, endLineNumber: number },
  14. isPreview?: boolean,
  15. }
  16. // It calls callback when GraphViewer is not null.
  17. // eslint-disable-next-line @typescript-eslint/ban-types
  18. const waitForGraphViewer = async(callback: Function) => {
  19. const MAX_WAIT_COUNT = 10; // no reason for 10
  20. for (let i = 0; i < MAX_WAIT_COUNT; i++) {
  21. if (isGraphViewer((window as CustomWindow).GraphViewer)) {
  22. callback((window as CustomWindow).GraphViewer);
  23. break;
  24. }
  25. // Sleep 500 ms
  26. // eslint-disable-next-line no-await-in-loop
  27. await new Promise<void>(r => setTimeout(() => r(), 500));
  28. }
  29. };
  30. const Drawio = (props: Props): JSX.Element => {
  31. const { t } = useTranslation();
  32. // Wrap with a function since GraphViewer is a function.
  33. // This applies when call setGraphViewer as well.
  34. const [GraphViewer, setGraphViewer] = useState<IGraphViewer | undefined>(() => (window as CustomWindow).GraphViewer);
  35. const { drawioContent, rangeLineNumberOfMarkdown, isPreview } = props;
  36. // const { open: openDrawioModal } = useDrawioModalForPage();
  37. const drawioContainerRef = useRef<HTMLDivElement>(null);
  38. const globalEmitter: EventEmitter = (window as CustomWindow).globalEmitter;
  39. const editButtonClickHandler = useCallback(() => {
  40. const { beginLineNumber, endLineNumber } = rangeLineNumberOfMarkdown;
  41. globalEmitter.emit('launchDrawioModal', beginLineNumber, endLineNumber);
  42. }, [rangeLineNumberOfMarkdown, globalEmitter]);
  43. const renderDrawio = useCallback((GraphViewer: IGraphViewer) => {
  44. if (drawioContainerRef.current == null) {
  45. return;
  46. }
  47. const mxgraphs = drawioContainerRef.current.getElementsByClassName('mxgraph');
  48. if (mxgraphs.length > 0) {
  49. // GROWI では、mxgraph element は最初のものをレンダリングする前提とする
  50. const div = mxgraphs[0];
  51. if (div != null) {
  52. div.innerHTML = '';
  53. GraphViewer.createViewerForElement(div);
  54. }
  55. }
  56. }, [drawioContainerRef]);
  57. const renderDrawioWithDebounce = useMemo(() => debounce(200, renderDrawio), [renderDrawio]);
  58. useEffect(() => {
  59. if (GraphViewer == null) {
  60. waitForGraphViewer((gv: IGraphViewer) => {
  61. setGraphViewer(() => gv);
  62. });
  63. return;
  64. }
  65. renderDrawioWithDebounce(GraphViewer);
  66. }, [renderDrawioWithDebounce, GraphViewer]);
  67. return (
  68. <div className="editable-with-drawio position-relative">
  69. { !isPreview && (
  70. <NotAvailableForGuest>
  71. <button type="button" className="drawio-iframe-trigger position-absolute btn btn-outline-secondary" onClick={editButtonClickHandler}>
  72. <i className="icon-note mr-1"></i>{t('Edit')}
  73. </button>
  74. </NotAvailableForGuest>
  75. ) }
  76. <div
  77. className="drawio"
  78. style={
  79. {
  80. borderRadius: 3,
  81. border: '1px solid #d7d7d7',
  82. margin: '20px 0',
  83. }
  84. }
  85. ref={drawioContainerRef}
  86. // eslint-disable-next-line react/no-danger
  87. dangerouslySetInnerHTML={{ __html: drawioContent }}
  88. >
  89. </div>
  90. </div>
  91. );
  92. };
  93. export default Drawio;