Просмотр исходного кода

Merge pull request #8340 from weseek/feat/135182-use-drawio-modal-in-editor

feat: Able to use drawio modal in editor
Yuki Takei 2 лет назад
Родитель
Сommit
4094b69548

+ 2 - 2
apps/app/src/client/services/side-effects/drawio-modal-launcher-for-view.ts

@@ -5,7 +5,7 @@ import EventEmitter from 'events';
 import type { DrawioEditByViewerProps } from '@growi/remark-drawio';
 
 import { useSaveOrUpdate } from '~/client/services/page-operation';
-import mdu from '~/components/PageEditor/MarkdownDrawioUtil';
+import { replaceDrawioInMarkdown } from '~/components/Page/markdown-drawio-util-for-view';
 import type { OptionsToSave } from '~/interfaces/page-operation';
 import { useShareLinkId } from '~/stores/context';
 import { useDrawioModal } from '~/stores/modal';
@@ -41,7 +41,7 @@ export const useDrawioModalLauncherForView = (opts?: {
     }
 
     const currentMarkdown = currentPage.revision.body;
-    const newMarkdown = mdu.replaceDrawioInMarkdown(drawioMxFile, currentMarkdown, bol, eol);
+    const newMarkdown = replaceDrawioInMarkdown(drawioMxFile, currentMarkdown, bol, eol);
 
     const grantUserGroupIds = currentPage.grantedGroups.map((g) => {
       return {

+ 1 - 1
apps/app/src/client/services/side-effects/handsontable-modal-launcher-for-view.ts

@@ -4,7 +4,7 @@ import EventEmitter from 'events';
 
 import MarkdownTable from '~/client/models/MarkdownTable';
 import { useSaveOrUpdate } from '~/client/services/page-operation';
-import { getMarkdownTableFromLine, replaceMarkdownTableInMarkdown } from '~/components/PageEditor/markdown-table-util-for-view';
+import { getMarkdownTableFromLine, replaceMarkdownTableInMarkdown } from '~/components/Page/markdown-table-util-for-view';
 import type { OptionsToSave } from '~/interfaces/page-operation';
 import { useShareLinkId } from '~/stores/context';
 import { useHandsontableModal } from '~/stores/modal';

+ 21 - 0
apps/app/src/components/Page/markdown-drawio-util-for-view.ts

@@ -0,0 +1,21 @@
+/**
+ * return markdown where the drawioData specified by line number params is replaced to the drawioData specified by drawioData param
+ */
+export const replaceDrawioInMarkdown = (drawioData: string, markdown: string, beginLineNumber: number, endLineNumber: number): string => {
+  const splitMarkdown = markdown.split(/\r\n|\r|\n/);
+  const markdownBeforeDrawio = splitMarkdown.slice(0, beginLineNumber - 1);
+  const markdownAfterDrawio = splitMarkdown.slice(endLineNumber);
+
+  let newMarkdown = '';
+  if (markdownBeforeDrawio.length > 0) {
+    newMarkdown += `${markdownBeforeDrawio.join('\n')}\n`;
+  }
+  newMarkdown += '``` drawio\n';
+  newMarkdown += drawioData;
+  newMarkdown += '\n```';
+  if (markdownAfterDrawio.length > 0) {
+    newMarkdown += `\n${markdownAfterDrawio.join('\n')}`;
+  }
+
+  return newMarkdown;
+};

+ 0 - 0
apps/app/src/components/PageEditor/markdown-table-util-for-view.ts → apps/app/src/components/Page/markdown-table-util-for-view.ts


+ 1 - 1
apps/app/src/components/PageEditor/DrawioCommunicationHelper.ts

@@ -29,7 +29,7 @@ export class DrawioCommunicationHelper {
     this.callbackOpts = callbackOpts;
   }
 
-  onReceiveMessage(event: MessageEvent, drawioMxFile: string): void {
+  onReceiveMessage(event: MessageEvent, drawioMxFile: string | null): void {
 
     // check origin
     if (event.origin != null && this.drawioUri != null) {

+ 22 - 9
apps/app/src/components/PageEditor/DrawioModal.tsx

@@ -4,12 +4,15 @@ import React, {
   useMemo,
 } from 'react';
 
+import { useCodeMirrorEditorIsolated } from '@growi/editor';
+import { useDrawioModalForEditor } from '@growi/editor/src/stores/use-drawio';
 import {
   Modal,
   ModalBody,
 } from 'reactstrap';
 
 import { getDiagramsNetLangCode } from '~/client/util/locale-utils';
+import { replaceFocusedDrawioWithEditor, getMarkdownDrawioMxfile } from '~/components/PageEditor/markdown-drawio-util-for-editor';
 import { useRendererConfig } from '~/stores/context';
 import { useDrawioModal } from '~/stores/modal';
 import { usePersonalSettings } from '~/stores/personal-settings';
@@ -47,6 +50,11 @@ export const DrawioModal = (): JSX.Element => {
   });
 
   const { data: drawioModalData, close: closeDrawioModal } = useDrawioModal();
+  const { data: drawioModalDataInEditor, close: closeDrawioModalInEditor } = useDrawioModalForEditor();
+  const editorKey = drawioModalDataInEditor?.editorKey ?? null;
+  const { data: codeMirrorEditor } = useCodeMirrorEditorIsolated(editorKey);
+  const editor = codeMirrorEditor?.view;
+  const isOpenedInEditor = (drawioModalDataInEditor?.isOpened ?? false) && (editor != null);
   const isOpened = drawioModalData?.isOpened ?? false;
 
   const drawioUriWithParams = useMemo(() => {
@@ -78,23 +86,28 @@ export const DrawioModal = (): JSX.Element => {
       return undefined;
     }
 
+    const save = editor != null ? (drawioMxFile: string) => {
+      replaceFocusedDrawioWithEditor(editor, drawioMxFile);
+    } : drawioModalData?.onSave;
+
     return new DrawioCommunicationHelper(
       rendererConfig.drawioUri,
       drawioConfig,
-      { onClose: closeDrawioModal, onSave: drawioModalData?.onSave },
+      { onClose: isOpened ? closeDrawioModal : closeDrawioModalInEditor, onSave: save },
     );
-  }, [closeDrawioModal, drawioModalData?.onSave, rendererConfig]);
+  }, [closeDrawioModal, closeDrawioModalInEditor, drawioModalData?.onSave, editor, isOpened, rendererConfig]);
 
   const receiveMessageHandler = useCallback((event: MessageEvent) => {
     if (drawioModalData == null) {
       return;
     }
 
-    drawioCommunicationHelper?.onReceiveMessage(event, drawioModalData.drawioMxFile);
-  }, [drawioCommunicationHelper, drawioModalData]);
+    const drawioMxFile = editor != null ? getMarkdownDrawioMxfile(editor) : drawioModalData.drawioMxFile;
+    drawioCommunicationHelper?.onReceiveMessage(event, drawioMxFile);
+  }, [drawioCommunicationHelper, drawioModalData, editor]);
 
   useEffect(() => {
-    if (isOpened) {
+    if (isOpened || isOpenedInEditor) {
       window.addEventListener('message', receiveMessageHandler);
     }
     else {
@@ -105,12 +118,12 @@ export const DrawioModal = (): JSX.Element => {
     return function() {
       window.removeEventListener('message', receiveMessageHandler);
     };
-  }, [isOpened, receiveMessageHandler]);
+  }, [isOpened, isOpenedInEditor, receiveMessageHandler]);
 
   return (
     <Modal
-      isOpen={isOpened}
-      toggle={() => closeDrawioModal()}
+      isOpen={isOpened || isOpenedInEditor}
+      toggle={() => (isOpened ? closeDrawioModal() : closeDrawioModalInEditor())}
       backdrop="static"
       className="drawio-modal grw-body-only-modal-expanded"
       size="xl"
@@ -126,7 +139,7 @@ export const DrawioModal = (): JSX.Element => {
         {/* iframe */}
         { drawioUriWithParams != null && (
           <div className="w-100 h-100 position-absolute d-flex">
-            { isOpened && (
+            { (isOpened || isOpenedInEditor) && (
               <iframe
                 src={drawioUriWithParams.href}
                 className="border-0 flex-grow-1"

+ 0 - 179
apps/app/src/components/PageEditor/MarkdownDrawioUtil.js

@@ -1,179 +0,0 @@
-/**
- * Utility for markdown drawio
- */
-class MarkdownDrawioUtil {
-
-  constructor() {
-    this.lineBeginPartOfDrawioRE = /^```(\s.*)drawio$/;
-    this.lineEndPartOfDrawioRE = /^```$/;
-  }
-
-  /**
-   * return the postion of the BOD(beginning of drawio)
-   * (If the BOD is not found after the cursor or the EOD is found before the BOD, return null)
-   */
-  getBod(editor) {
-    const curPos = editor.getCursor();
-    const firstLine = editor.getDoc().firstLine();
-
-    if (this.lineBeginPartOfDrawioRE.test(editor.getDoc().getLine(curPos.line))) {
-      return { line: curPos.line, ch: 0 };
-    }
-
-    let line = curPos.line - 1;
-    let isFound = false;
-    for (; line >= firstLine; line--) {
-      const strLine = editor.getDoc().getLine(line);
-      if (this.lineBeginPartOfDrawioRE.test(strLine)) {
-        isFound = true;
-        break;
-      }
-
-      if (this.lineEndPartOfDrawioRE.test(strLine)) {
-        isFound = false;
-        break;
-      }
-    }
-
-    if (!isFound) {
-      return null;
-    }
-
-    const bodLine = Math.max(firstLine, line);
-    return { line: bodLine, ch: 0 };
-  }
-
-  /**
-   * return the postion of the EOD(end of drawio)
-   * (If the EOD is not found after the cursor or the BOD is found before the EOD, return null)
-   */
-  getEod(editor) {
-    const curPos = editor.getCursor();
-    const lastLine = editor.getDoc().lastLine();
-
-    if (this.lineEndPartOfDrawioRE.test(editor.getDoc().getLine(curPos.line))) {
-      return { line: curPos.line, ch: editor.getDoc().getLine(curPos.line).length };
-    }
-
-    let line = curPos.line + 1;
-    let isFound = false;
-    for (; line <= lastLine; line++) {
-      const strLine = editor.getDoc().getLine(line);
-      if (this.lineEndPartOfDrawioRE.test(strLine)) {
-        isFound = true;
-        break;
-      }
-
-      if (this.lineBeginPartOfDrawioRE.test(strLine)) {
-        isFound = false;
-        break;
-      }
-    }
-
-    if (!isFound) {
-      return null;
-    }
-
-    const eodLine = Math.min(line, lastLine);
-    const lineLength = editor.getDoc().getLine(eodLine).length;
-    return { line: eodLine, ch: lineLength };
-  }
-
-  /**
-   * return boolean value whether the cursor position is in a drawio
-   */
-  isInDrawioBlock(editor) {
-    const bod = this.getBod(editor);
-    const eod = this.getEod(editor);
-    if (bod === null || eod === null) {
-      return false;
-    }
-    return JSON.stringify(bod) !== JSON.stringify(eod);
-  }
-
-  /**
-   * return drawioData instance where the cursor is
-   * (If the cursor is not in a drawio block, return null)
-   */
-  getMarkdownDrawioMxfile(editor) {
-    if (this.isInDrawioBlock(editor)) {
-      const bod = this.getBod(editor);
-      const eod = this.getEod(editor);
-
-      // skip block begin sesion("``` drawio")
-      bod.line++;
-      // skip block end sesion("```")
-      eod.line--;
-      eod.ch = editor.getDoc().getLine(eod.line).length;
-
-      return editor.getDoc().getRange(bod, eod);
-    }
-    return null;
-  }
-
-  replaceFocusedDrawioWithEditor(editor, drawioData) {
-    const curPos = editor.getCursor();
-    const drawioBlock = ['``` drawio', drawioData.toString(), '```'].join('\n');
-    let beginPos;
-    let endPos;
-
-    if (this.isInDrawioBlock(editor)) {
-      beginPos = this.getBod(editor);
-      endPos = this.getEod(editor);
-    }
-    else {
-      beginPos = { line: curPos.line, ch: curPos.ch };
-      endPos = { line: curPos.line, ch: curPos.ch };
-    }
-
-    editor.getDoc().replaceRange(drawioBlock, beginPos, endPos);
-  }
-
-  /**
-   * return markdown where the drawioData specified by line number params is replaced to the drawioData specified by drawioData param
-   * @param {string} drawioData
-   * @param {string} markdown
-   * @param beginLineNumber
-   * @param endLineNumber
-   */
-  replaceDrawioInMarkdown(drawioData, markdown, beginLineNumber, endLineNumber) {
-    const splitMarkdown = markdown.split(/\r\n|\r|\n/);
-    const markdownBeforeDrawio = splitMarkdown.slice(0, beginLineNumber - 1);
-    const markdownAfterDrawio = splitMarkdown.slice(endLineNumber);
-
-    let newMarkdown = '';
-    if (markdownBeforeDrawio.length > 0) {
-      newMarkdown += `${markdownBeforeDrawio.join('\n')}\n`;
-    }
-    newMarkdown += '``` drawio\n';
-    newMarkdown += drawioData;
-    newMarkdown += '\n```';
-    if (markdownAfterDrawio.length > 0) {
-      newMarkdown += `\n${markdownAfterDrawio.join('\n')}`;
-    }
-
-    return newMarkdown;
-  }
-
-  /**
-   * return an array of the starting line numbers of the drawio sections found in markdown
-   */
-  findAllDrawioSection(editor) {
-    const lineNumbers = [];
-    // refs: https://github.com/codemirror/CodeMirror/blob/5.64.0/addon/fold/foldcode.js#L106-L111
-    for (let i = editor.firstLine(), e = editor.lastLine(); i <= e; i++) {
-      const line = editor.getLine(i);
-      const match = this.lineBeginPartOfDrawioRE.exec(line);
-      if (match) {
-        lineNumbers.push(i);
-      }
-    }
-    return lineNumbers;
-  }
-
-}
-
-// singleton pattern
-const instance = new MarkdownDrawioUtil();
-Object.freeze(instance);
-export default instance;

+ 134 - 0
apps/app/src/components/PageEditor/markdown-drawio-util-for-editor.ts

@@ -0,0 +1,134 @@
+import { EditorView } from '@codemirror/view';
+
+const lineBeginPartOfDrawioRE = /^```(\s.*)drawio$/;
+const lineEndPartOfDrawioRE = /^```$/;
+const firstLineNum = 1;
+
+const curPos = (editor: EditorView) => {
+  return editor.state.selection.main.head;
+};
+
+const doc = (editor: EditorView) => {
+  return editor.state.doc;
+};
+
+const lastLineNum = (editor: EditorView) => {
+  return doc(editor).lines;
+};
+
+const getCursorLine = (editor: EditorView) => {
+  return doc(editor).lineAt(curPos(editor));
+};
+
+const getLine = (editor: EditorView, lineNum: number) => {
+  return doc(editor).line(lineNum);
+};
+
+/**
+ * return the postion of the BOD(beginning of drawio)
+ * (If the BOD is not found after the cursor or the EOD is found before the BOD, return null)
+ */
+const getBod = (editor: EditorView) => {
+  const strLine = getCursorLine(editor).text;
+  if (lineBeginPartOfDrawioRE.test(strLine)) {
+    // get the beginning of the line where the cursor is located
+    return getCursorLine(editor).from;
+  }
+
+  let line = getCursorLine(editor).number - 1;
+  let isFound = false;
+  for (; line >= firstLineNum; line--) {
+    const strLine = getLine(editor, line).text;
+    if (lineBeginPartOfDrawioRE.test(strLine)) {
+      isFound = true;
+      break;
+    }
+
+    if (lineEndPartOfDrawioRE.test(strLine)) {
+      isFound = false;
+      break;
+    }
+  }
+
+  if (!isFound) {
+    return null;
+  }
+
+  const botLine = Math.max(firstLineNum, line);
+  return getLine(editor, botLine).from;
+};
+
+/**
+ * return the postion of the EOD(end of drawio)
+ * (If the EOD is not found after the cursor or the BOD is found before the EOD, return null)
+ */
+const getEod = (editor: EditorView) => {
+  const lastLine = lastLineNum(editor);
+
+  const strLine = getCursorLine(editor).text;
+  if (lineEndPartOfDrawioRE.test(strLine)) {
+    // get the end of the line where the cursor is located
+    return getCursorLine(editor).to;
+  }
+
+  let line = getCursorLine(editor).number + 1;
+  let isFound = false;
+  for (; line <= lastLine; line++) {
+    const strLine = getLine(editor, line).text;
+    if (lineEndPartOfDrawioRE.test(strLine)) {
+      isFound = true;
+      break;
+    }
+
+    if (lineBeginPartOfDrawioRE.test(strLine)) {
+      isFound = false;
+      break;
+    }
+  }
+
+  if (!isFound) {
+    return null;
+  }
+
+  const eodLine = Math.min(line, lastLine);
+  return getLine(editor, eodLine).to;
+};
+
+/**
+ * return drawioData instance where the cursor is
+ * (If the cursor is not in a drawio block, return null)
+ */
+export const getMarkdownDrawioMxfile = (editor: EditorView): string | null => {
+  const bod = getBod(editor);
+  const eod = getEod(editor);
+  if (bod == null || eod == null || JSON.stringify(bod) === JSON.stringify(eod)) {
+    return null;
+  }
+
+  // skip block begin sesion("``` drawio")
+  const bodLineNum = doc(editor).lineAt(bod).number + 1;
+  const bodLine = getLine(editor, bodLineNum).from;
+  // skip block end sesion("```")
+  const eodLineNum = doc(editor).lineAt(eod).number - 1;
+  const eodLine = getLine(editor, eodLineNum).to;
+
+  return editor.state.sliceDoc(bodLine, eodLine);
+};
+
+export const replaceFocusedDrawioWithEditor = (editor: EditorView, drawioData: string): void => {
+  const drawioBlock = ['``` drawio', drawioData.toString(), '```'].join('\n');
+  let bod = getBod(editor);
+  let eod = getEod(editor);
+  if (bod == null || eod == null || JSON.stringify(bod) === JSON.stringify(eod)) {
+    bod = curPos(editor);
+    eod = curPos(editor);
+  }
+
+  editor.dispatch({
+    changes: {
+      from: bod,
+      to: eod,
+      insert: drawioBlock,
+    },
+  });
+};

+ 15 - 2
packages/editor/src/components/CodeMirrorEditor/Toolbar/DiagramButton.tsx

@@ -1,6 +1,19 @@
-export const DiagramButton = (): JSX.Element => {
+import { useCallback } from 'react';
+
+import { useDrawioModalForEditor } from '../../../stores/use-drawio';
+
+type Props = {
+  editorKey: string,
+}
+
+export const DiagramButton = (props: Props): JSX.Element => {
+  const { editorKey } = props;
+  const { open: openDrawioModal } = useDrawioModalForEditor();
+  const onClickDiagramButton = useCallback(() => {
+    openDrawioModal(editorKey);
+  }, [editorKey, openDrawioModal]);
   return (
-    <button type="button" className="btn btn-toolbar-button">
+    <button type="button" className="btn btn-toolbar-button" onClick={onClickDiagramButton}>
       <span className="material-symbols-outlined fs-5">lan</span>
     </button>
   );

+ 1 - 1
packages/editor/src/components/CodeMirrorEditor/Toolbar/Toolbar.tsx

@@ -28,7 +28,7 @@ export const Toolbar = memo((props: Props): JSX.Element => {
         editorKey={editorKey}
       />
       <TableButton editorKey={editorKey} />
-      <DiagramButton />
+      <DiagramButton editorKey={editorKey} />
       <TemplateButton editorKey={editorKey} />
     </div>
   );

+ 4 - 0
packages/editor/src/services/codemirror-editor/use-codemirror-editor/use-codemirror-editor.ts

@@ -14,6 +14,7 @@ import { emojiAutocompletionSettings } from '../../extensions/emojiAutocompletio
 
 import { useAppendExtensions, type AppendExtensions } from './utils/append-extensions';
 import { useFocus, type Focus } from './utils/focus';
+import { FoldDrawio, useFoldDrawio } from './utils/fold-drawio';
 import { useGetDoc, type GetDoc } from './utils/get-doc';
 import { useInitDoc, type InitDoc } from './utils/init-doc';
 import { useInsertMarkdownElements, type InsertMarkdowElements } from './utils/insert-markdown-elements';
@@ -41,6 +42,7 @@ type UseCodeMirrorEditorUtils = {
   replaceText: ReplaceText,
   insertMarkdownElements: InsertMarkdowElements,
   insertPrefix: InsertPrefix,
+  foldDrawio: FoldDrawio,
 }
 export type UseCodeMirrorEditor = {
   state: EditorState | undefined;
@@ -95,6 +97,7 @@ export const useCodeMirrorEditor = (props?: UseCodeMirror): UseCodeMirrorEditor
   const replaceText = useReplaceText(view);
   const insertMarkdownElements = useInsertMarkdownElements(view);
   const insertPrefix = useInsertPrefix(view);
+  const foldDrawio = useFoldDrawio(view);
 
   return {
     state,
@@ -108,5 +111,6 @@ export const useCodeMirrorEditor = (props?: UseCodeMirror): UseCodeMirrorEditor
     replaceText,
     insertMarkdownElements,
     insertPrefix,
+    foldDrawio,
   };
 };

+ 50 - 0
packages/editor/src/services/codemirror-editor/use-codemirror-editor/utils/fold-drawio.ts

@@ -0,0 +1,50 @@
+import { useEffect } from 'react';
+
+import { foldEffect } from '@codemirror/language';
+import { EditorView } from '@codemirror/view';
+
+export type FoldDrawio = void;
+
+const findAllDrawioSection = (view?: EditorView) => {
+  if (view == null) {
+    return;
+  }
+  const lineBeginPartOfDrawioRE = /^```(\s.*)drawio$/;
+  const lineNumbers: number[] = [];
+  // repeat the process in each line from the top to the bottom in the editor
+  for (let i = 1, e = view.state.doc.lines; i <= e; i++) {
+    // get each line text
+    const lineTxt = view.state.doc.line(i).text;
+    const match = lineBeginPartOfDrawioRE.exec(lineTxt);
+    if (match) {
+      lineNumbers.push(i);
+    }
+  }
+  return lineNumbers;
+};
+
+const foldDrawioSection = (lineNumbers?: number[], view?: EditorView) => {
+  if (view == null || lineNumbers == null) {
+    return;
+  }
+  lineNumbers.forEach((lineNumber) => {
+    // get the end of the lines containing '''drawio
+    const from = view.state.doc.line(lineNumber).to;
+    // get the end of the lines containing '''
+    const to = view.state.doc.line(lineNumber + 2).to;
+    view?.dispatch({
+      effects: foldEffect.of({
+        from,
+        to,
+      }),
+    });
+  });
+};
+
+export const useFoldDrawio = (view?: EditorView): FoldDrawio => {
+  const lineNumbers = findAllDrawioSection(view);
+
+  useEffect(() => {
+    foldDrawioSection(lineNumbers, view);
+  }, [view, lineNumbers]);
+};

+ 40 - 0
packages/editor/src/stores/use-drawio.ts

@@ -0,0 +1,40 @@
+import { useCallback } from 'react';
+
+import { useSWRStatic } from '@growi/core/dist/swr';
+import type { SWRResponse } from 'swr';
+
+type DrawioModalStatus = {
+  isOpened: boolean,
+  editorKey: string | undefined,
+}
+
+type DrawioModalStatusUtils = {
+  open(
+    editorKey: string,
+  ): void,
+  close(): void,
+}
+
+export const useDrawioModalForEditor = (status?: DrawioModalStatus): SWRResponse<DrawioModalStatus, Error> & DrawioModalStatusUtils => {
+  const initialData: DrawioModalStatus = {
+    isOpened: false,
+    editorKey: undefined,
+  };
+  const swrResponse = useSWRStatic<DrawioModalStatus, Error>('drawioModalStatusForEditor', status, { fallbackData: initialData });
+
+  const { mutate } = swrResponse;
+
+  const open = useCallback((editorKey: string | undefined): void => {
+    mutate({ isOpened: true, editorKey });
+  }, [mutate]);
+
+  const close = useCallback((): void => {
+    mutate({ isOpened: false, editorKey: undefined });
+  }, [mutate]);
+
+  return {
+    ...swrResponse,
+    open,
+    close,
+  };
+};