Browse Source

Merge branch 'dev/7.0.x' into feat/135182-use-drawio-modal-in-editor

soumaeda 2 years ago
parent
commit
68fe595d33

+ 15 - 13
apps/app/src/components/PageEditor/MarkdownTableInterceptor.js → apps/app/_obsolete/src/components/PageEditor/MarkdownTableInterceptor.js

@@ -2,7 +2,10 @@ import { BasicInterceptor } from '@growi/core/dist/utils';
 
 import MarkdownTable from '~/client/models/MarkdownTable';
 
-import mtu from './MarkdownTableUtil';
+import {
+  getStrFromBot, addRowToMarkdownTable, getStrToEot, isEndOfLine, mergeMarkdownTable, replaceFocusedMarkdownTableWithEditor,
+  isInTable, emptyLineOfTableRE,
+} from '../../../../src/components/PageEditor/markdown-table-util-for-editor';
 
 /**
  * Interceptor for markdown table
@@ -27,24 +30,24 @@ export default class MarkdownTableInterceptor extends BasicInterceptor {
 
   addRow(cm) {
     // get lines all of table from current position to beginning of table
-    const strFromBot = mtu.getStrFromBot(cm);
+    const strFromBot = getStrFromBot(cm);
     let table = MarkdownTable.fromMarkdownString(strFromBot);
 
-    mtu.addRowToMarkdownTable(table);
+    addRowToMarkdownTable(table);
 
-    const strToEot = mtu.getStrToEot(cm);
+    const strToEot = getStrToEot(cm);
     const tableBottom = MarkdownTable.fromMarkdownString(strToEot);
     if (tableBottom.table.length > 0) {
-      table = mtu.mergeMarkdownTable([table, tableBottom]);
+      table = mergeMarkdownTable([table, tableBottom]);
     }
 
-    mtu.replaceMarkdownTableWithReformed(cm, table);
+    replaceFocusedMarkdownTableWithEditor(cm, table);
   }
 
   reformTable(cm) {
-    const tableStr = mtu.getStrFromBot(cm) + mtu.getStrToEot(cm);
+    const tableStr = getStrFromBot(cm) + getStrToEot(cm);
     const table = MarkdownTable.fromMarkdownString(tableStr);
-    mtu.replaceMarkdownTableWithReformed(cm, table);
+    replaceFocusedMarkdownTableWithEditor(cm, table);
   }
 
   removeRow(editor) {
@@ -67,16 +70,15 @@ export default class MarkdownTableInterceptor extends BasicInterceptor {
 
     const cm = editor.getCodeMirror();
 
-    const isInTable = mtu.isInTable(cm);
-    const isLastRow = mtu.getStrToEot(cm) === editor.getStrToEol();
+    const isLastRow = getStrToEot(cm) === editor.getStrToEol();
 
-    if (isInTable) {
+    if (isInTable(cm)) {
       // at EOL in the table
-      if (mtu.isEndOfLine(cm)) {
+      if (isEndOfLine(cm)) {
         this.addRow(cm);
       }
       // last empty row
-      else if (isLastRow && mtu.emptyLineOfTableRE.test(editor.getStrFromBol() + editor.getStrToEol())) {
+      else if (isLastRow && emptyLineOfTableRE.test(editor.getStrFromBol() + editor.getStrToEol())) {
         this.removeRow(editor);
       }
       else {

+ 4 - 4
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 mtu from '~/components/PageEditor/MarkdownTableUtil';
+import { getMarkdownTableFromLine, replaceMarkdownTableInMarkdown } from '~/components/PageEditor/markdown-table-util-for-view';
 import type { OptionsToSave } from '~/interfaces/page-operation';
 import { useShareLinkId } from '~/stores/context';
 import { useHandsontableModal } from '~/stores/modal';
@@ -40,7 +40,7 @@ export const useHandsontableModalLauncherForView = (opts?: {
     }
 
     const currentMarkdown = currentPage.revision.body;
-    const newMarkdown = mtu.replaceMarkdownTableInMarkdown(table, currentMarkdown, bol, eol);
+    const newMarkdown = replaceMarkdownTableInMarkdown(table, currentMarkdown, bol, eol);
 
     const grantUserGroupIds = currentPage.grantedGroups.map((g) => {
       return {
@@ -82,8 +82,8 @@ export const useHandsontableModalLauncherForView = (opts?: {
 
     const handler = (bol: number, eol: number) => {
       const markdown = currentPage.revision.body;
-      const currentMarkdownTable = mtu.getMarkdownTableFromLine(markdown, bol, eol);
-      openHandsontableModal(currentMarkdownTable, undefined, false, table => saveByHandsontableModal(table, bol, eol));
+      const currentMarkdownTable = getMarkdownTableFromLine(markdown, bol, eol);
+      openHandsontableModal(currentMarkdownTable, false, table => saveByHandsontableModal(table, bol, eol));
     };
     globalEmitter.on('launchHandsonTableModal', handler);
 

+ 13 - 6
apps/app/src/components/PageEditor/HandsontableModal.tsx

@@ -1,5 +1,6 @@
 import React, { useState } from 'react';
 
+import { useHandsontableModalForEditor } from '@growi/editor/src/stores/use-handsontable';
 import { HotTable } from '@handsontable/react';
 import Handsontable from 'handsontable';
 import { useTranslation } from 'next-i18next';
@@ -10,7 +11,7 @@ import {
 import { debounce } from 'throttle-debounce';
 
 import MarkdownTable from '~/client/models/MarkdownTable';
-import mtu from '~/components/PageEditor/MarkdownTableUtil';
+import { replaceFocusedMarkdownTableWithEditor, getMarkdownTable } from '~/components/PageEditor/markdown-table-util-for-editor';
 import { useHandsontableModal } from '~/stores/modal';
 
 import ExpandOrContractButton from '../ExpandOrContractButton';
@@ -32,11 +33,13 @@ export const HandsontableModal = (): JSX.Element => {
 
   const { t } = useTranslation('commons');
   const { data: handsontableModalData, close: closeHandsontableModal } = useHandsontableModal();
+  const { data: handsontableModalForEditorData } = useHandsontableModalForEditor();
 
   const isOpened = handsontableModalData?.isOpened ?? false;
+  const isOpendInEditor = handsontableModalForEditorData?.isOpened ?? false;
   const table = handsontableModalData?.table;
   const autoFormatMarkdownTable = handsontableModalData?.autoFormatMarkdownTable ?? false;
-  const editor = handsontableModalData?.editor;
+  const editor = handsontableModalForEditorData?.editor;
   const onSave = handsontableModalData?.onSave;
 
   const defaultMarkdownTable = () => {
@@ -102,8 +105,9 @@ export const HandsontableModal = (): JSX.Element => {
   const debouncedHandleWindowExpandedChange = debounce(100, handleWindowExpandedChange);
 
   const handleModalOpen = () => {
-    const initTableInstance = table == null ? defaultMarkdownTable : table.clone();
-    setMarkdownTable(table ?? defaultMarkdownTable);
+    const markdownTableState = table == null && editor != null ? getMarkdownTable(editor) : table;
+    const initTableInstance = markdownTableState == null ? defaultMarkdownTable : markdownTableState.clone();
+    setMarkdownTable(markdownTableState ?? defaultMarkdownTable);
     setMarkdownTableOnInit(initTableInstance);
     debouncedHandleWindowExpandedChange();
   };
@@ -163,7 +167,10 @@ export const HandsontableModal = (): JSX.Element => {
       return;
     }
 
-    mtu.replaceFocusedMarkdownTableWithEditor(editor, newMarkdownTable);
+    if (editor == null) {
+      return;
+    }
+    replaceFocusedMarkdownTableWithEditor(editor, newMarkdownTable);
     cancel();
   };
 
@@ -441,7 +448,7 @@ export const HandsontableModal = (): JSX.Element => {
 
   return (
     <Modal
-      isOpen={isOpened}
+      isOpen={isOpened || isOpendInEditor}
       toggle={cancel}
       backdrop="static"
       keyboard={false}

+ 1 - 1
apps/app/src/components/PageEditor/MarkdownTableDataImportForm.tsx

@@ -56,7 +56,7 @@ export const MarkdownTableDataImportForm = (props: MarkdownTableDataImportFormPr
         <label htmlFor="data-import-form-type-select" className="form-label">{t('select_data_format')}</label>
         <select
           id="data-import-form-type-select"
-          className="form-control"
+          className="form-select"
           placeholder="select"
           value={dataFormat}
           onChange={(e) => { return setDataFormat(e.target.value) }}

+ 0 - 190
apps/app/src/components/PageEditor/MarkdownTableUtil.js

@@ -1,190 +0,0 @@
-import MarkdownTable from '~/client/models/MarkdownTable';
-
-/**
- * Utility for markdown table
- */
-class MarkdownTableUtil {
-
-  constructor() {
-    // https://github.com/markdown-it/markdown-it/blob/d29f421927e93e88daf75f22089a3e732e195bd2/lib/rules_block/table.js#L83
-    this.tableAlignmentLineRE = /^[-:|][-:|\s]*$/;
-    this.tableAlignmentLineNegRE = /^[^-:]*$/; // it is need to check to ignore empty row which is matched above RE
-    // https://regex101.com/r/7BN2fR/10
-    this.linePartOfTableRE = /^([^\r\n|]*)\|(([^\r\n|]*\|)+)$/;
-    // https://regex101.com/r/1UuWBJ/3
-    this.emptyLineOfTableRE = /^([^\r\n|]*)\|((\s*\|)+)$/;
-
-    this.getEot = this.getEot.bind(this);
-    this.getStrFromBot = this.getStrFromBot.bind(this);
-    this.getStrToEot = this.getStrToEot.bind(this);
-    this.isInTable = this.isInTable.bind(this);
-    this.replaceFocusedMarkdownTableWithEditor = this.replaceFocusedMarkdownTableWithEditor.bind(this);
-    this.replaceMarkdownTableWithReformed = this.replaceFocusedMarkdownTableWithEditor; // alias
-  }
-
-  /**
-   * return the postion of the BOT(beginning of table)
-   * (If the cursor is not in a table, return its position)
-   */
-  getBot(editor) {
-    const curPos = editor.getCursor();
-    if (!this.isInTable(editor)) {
-      return { line: curPos.line, ch: curPos.ch };
-    }
-
-    const firstLine = editor.getDoc().firstLine();
-    let line = curPos.line - 1;
-    for (; line >= firstLine; line--) {
-      const strLine = editor.getDoc().getLine(line);
-      if (!this.linePartOfTableRE.test(strLine)) {
-        break;
-      }
-    }
-    const botLine = Math.max(firstLine, line + 1);
-    return { line: botLine, ch: 0 };
-  }
-
-  /**
-   * return the postion of the EOT(end of table)
-   * (If the cursor is not in a table, return its position)
-   */
-  getEot(editor) {
-    const curPos = editor.getCursor();
-    if (!this.isInTable(editor)) {
-      return { line: curPos.line, ch: curPos.ch };
-    }
-
-    const lastLine = editor.getDoc().lastLine();
-    let line = curPos.line + 1;
-    for (; line <= lastLine; line++) {
-      const strLine = editor.getDoc().getLine(line);
-      if (!this.linePartOfTableRE.test(strLine)) {
-        break;
-      }
-    }
-    const eotLine = Math.min(line - 1, lastLine);
-    const lineLength = editor.getDoc().getLine(eotLine).length;
-    return { line: eotLine, ch: lineLength };
-  }
-
-  /**
-   * return strings from BOT(beginning of table) to the cursor position
-   */
-  getStrFromBot(editor) {
-    const curPos = editor.getCursor();
-    return editor.getDoc().getRange(this.getBot(editor), curPos);
-  }
-
-  /**
-   * return strings from the cursor position to EOT(end of table)
-   */
-  getStrToEot(editor) {
-    const curPos = editor.getCursor();
-    return editor.getDoc().getRange(curPos, this.getEot(editor));
-  }
-
-  /**
-   * return MarkdownTable instance of the table where the cursor is
-   * (If the cursor is not in a table, return null)
-   */
-  getMarkdownTable(editor) {
-    if (!this.isInTable(editor)) {
-      return null;
-    }
-
-    const strFromBotToEot = editor.getDoc().getRange(this.getBot(editor), this.getEot(editor));
-    return MarkdownTable.fromMarkdownString(strFromBotToEot);
-  }
-
-  getMarkdownTableFromLine(markdown, bol, eol) {
-    const tableLines = markdown.split(/\r\n|\r|\n/).slice(bol - 1, eol).join('\n');
-    return MarkdownTable.fromMarkdownString(tableLines);
-  }
-
-  /**
-   * return boolean value whether the cursor position is end of line
-   */
-  isEndOfLine(editor) {
-    const curPos = editor.getCursor();
-    return (curPos.ch === editor.getDoc().getLine(curPos.line).length);
-  }
-
-  /**
-   * return boolean value whether the cursor position is in a table
-   */
-  isInTable(editor) {
-    const curPos = editor.getCursor();
-    return this.linePartOfTableRE.test(editor.getDoc().getLine(curPos.line));
-  }
-
-  /**
-   * add a row at the end
-   * (This function overwrite directory markdown table specified as argument.)
-   * @param {MarkdownTable} markdown table
-   */
-  addRowToMarkdownTable(mdtable) {
-    const numCol = mdtable.table.length > 0 ? mdtable.table[0].length : 1;
-    const newRow = [];
-    (new Array(numCol)).forEach(() => { return newRow.push('') }); // create cols
-    mdtable.table.push(newRow);
-  }
-
-  /**
-   * return markdown table that is merged all of markdown table in array
-   * (The merged markdown table options are used for the first markdown table.)
-   * @param {Array} array of markdown table
-   */
-  mergeMarkdownTable(mdtableList) {
-    if (mdtableList == null || !(mdtableList instanceof Array)) {
-      return undefined;
-    }
-
-    let newTable = [];
-    const options = mdtableList[0].options; // use option of first markdown-table
-    mdtableList.forEach((mdtable) => {
-      newTable = newTable.concat(mdtable.table);
-    });
-    return (new MarkdownTable(newTable, options));
-  }
-
-  /**
-   * replace focused markdown table with editor
-   * (A replaced table is reformed by markdown-table.)
-   * @param {MarkdownTable} table
-   */
-  replaceFocusedMarkdownTableWithEditor(editor, table) {
-    const curPos = editor.getCursor();
-    editor.getDoc().replaceRange(table.toString(), this.getBot(editor), this.getEot(editor));
-    editor.getDoc().setCursor(curPos.line + 1, 2);
-  }
-
-  /**
-   * return markdown where the markdown table specified by line number params is replaced to the markdown table specified by table param
-   * @param {string} markdown
-   * @param {MarkdownTable} table
-   * @param beginLineNumber
-   * @param endLineNumber
-   */
-  replaceMarkdownTableInMarkdown(table, markdown, beginLineNumber, endLineNumber) {
-    const splitMarkdown = markdown.split(/\r\n|\r|\n/);
-    const markdownBeforeTable = splitMarkdown.slice(0, beginLineNumber - 1);
-    const markdownAfterTable = splitMarkdown.slice(endLineNumber);
-
-    let newMarkdown = '';
-    if (markdownBeforeTable.length > 0) {
-      newMarkdown += `${markdownBeforeTable.join('\n')}\n`;
-    }
-    newMarkdown += table;
-    if (markdownAfterTable.length > 0) {
-      newMarkdown += `\n${markdownAfterTable.join('\n')}`;
-    }
-
-    return newMarkdown;
-  }
-
-}
-
-// singleton pattern
-const instance = new MarkdownTableUtil();
-Object.freeze(instance);
-export default instance;

+ 147 - 0
apps/app/src/components/PageEditor/markdown-table-util-for-editor.ts

@@ -0,0 +1,147 @@
+import type { EditorView } from '@codemirror/view';
+
+import MarkdownTable from '~/client/models/MarkdownTable';
+
+// https://regex101.com/r/7BN2fR/10
+const linePartOfTableRE = /^([^\r\n|]*)\|(([^\r\n|]*\|)+)$/;
+// https://regex101.com/r/1UuWBJ/3
+export const emptyLineOfTableRE = /^([^\r\n|]*)\|((\s*\|)+)$/;
+
+const curPos = (editor: EditorView): number => {
+  return editor.state.selection.main.head;
+};
+
+/**
+   * return boolean value whether the cursor position is in a table
+   */
+export const isInTable = (editor: EditorView): boolean => {
+  const lineText = editor.state.doc.lineAt(curPos(editor)).text;
+  return linePartOfTableRE.test(lineText);
+};
+
+/**
+   * return the postion of the BOT(beginning of table)
+   * (If the cursor is not in a table, return its position)
+   */
+const getBot = (editor: EditorView): number => {
+  if (!isInTable(editor)) {
+    return curPos(editor);
+  }
+
+  const doc = editor.state.doc;
+  const firstLine = 1;
+  let line = doc.lineAt(curPos(editor)).number - 1;
+  for (; line >= firstLine; line--) {
+    const strLine = doc.line(line).text;
+    if (!linePartOfTableRE.test(strLine)) {
+      break;
+    }
+  }
+  const botLine = Math.max(firstLine, line + 1);
+  return doc.line(botLine).from;
+};
+
+/**
+   * return the postion of the EOT(end of table)
+   * (If the cursor is not in a table, return its position)
+   */
+const getEot = (editor: EditorView): number => {
+  if (!isInTable(editor)) {
+    return curPos(editor);
+  }
+
+  const doc = editor.state.doc;
+  const lastLine = doc.lines;
+  let line = doc.lineAt(curPos(editor)).number + 1;
+  for (; line <= lastLine; line++) {
+    const strLine = doc.line(line).text;
+    if (!linePartOfTableRE.test(strLine)) {
+      break;
+    }
+  }
+  const eotLine = Math.min(line - 1, lastLine);
+  return doc.line(eotLine).to;
+};
+
+/**
+   * return strings from BOT(beginning of table) to the cursor position
+   */
+export const getStrFromBot = (editor: EditorView): string => {
+  return editor.state.sliceDoc(getBot(editor), curPos(editor));
+};
+
+/**
+   * return strings from the cursor position to EOT(end of table)
+   */
+export const getStrToEot = (editor: EditorView): string => {
+  return editor.state.sliceDoc(curPos(editor), getEot(editor));
+};
+
+/**
+   * return MarkdownTable instance of the table where the cursor is
+   * (If the cursor is not in a table, return null)
+   */
+export const getMarkdownTable = (editor: EditorView): MarkdownTable | undefined => {
+  if (!isInTable(editor)) {
+    return;
+  }
+
+  const strFromBotToEot = editor.state.sliceDoc(getBot(editor), getEot(editor));
+  return MarkdownTable.fromMarkdownString(strFromBotToEot);
+};
+
+/**
+   * return boolean value whether the cursor position is end of line
+   */
+export const isEndOfLine = (editor: EditorView): boolean => {
+  return curPos(editor) === editor.state.doc.lineAt(curPos(editor)).to;
+};
+
+/**
+   * add a row at the end
+   * (This function overwrite directory markdown table specified as argument.)
+   */
+export const addRowToMarkdownTable = (mdtable: MarkdownTable): any => {
+  const numCol = mdtable.table.length > 0 ? mdtable.table[0].length : 1;
+  const newRow: string[] = [];
+  (new Array(numCol)).forEach(() => { return newRow.push('') }); // create cols
+  mdtable.table.push(newRow);
+};
+
+/**
+   * return markdown table that is merged all of markdown table in array
+   * (The merged markdown table options are used for the first markdown table.)
+   */
+export const mergeMarkdownTable = (mdtableList: MarkdownTable): MarkdownTable | undefined => {
+  if (mdtableList == null || !(mdtableList instanceof Array)) {
+    return undefined;
+  }
+
+  let newTable = [];
+  const options = mdtableList[0].options; // use option of first markdown-table
+  mdtableList.forEach((mdtable) => {
+    newTable = newTable.concat(mdtable.table);
+  });
+  return (new MarkdownTable(newTable, options));
+};
+
+/**
+   * replace focused markdown table with editor
+   * (A replaced table is reformed by markdown-table.)
+   */
+export const replaceFocusedMarkdownTableWithEditor = (editor: EditorView, table: MarkdownTable): void => {
+  const botPos = getBot(editor);
+  const eotPos = getEot(editor);
+
+  editor.dispatch({
+    changes: {
+      from: botPos,
+      to: eotPos,
+      insert: table.toString(),
+    },
+  });
+  editor.dispatch({
+    selection: { anchor: editor.state.doc.lineAt(curPos(editor)).to },
+  });
+  editor.focus();
+};

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

@@ -0,0 +1,26 @@
+import MarkdownTable from '~/client/models/MarkdownTable';
+
+export const getMarkdownTableFromLine = (markdown: string, bol: number, eol: number): MarkdownTable => {
+  const tableLines = markdown.split(/\r\n|\r|\n/).slice(bol - 1, eol).join('\n');
+  return MarkdownTable.fromMarkdownString(tableLines);
+};
+
+/**
+   * return markdown where the markdown table specified by line number params is replaced to the markdown table specified by table param
+   */
+export const replaceMarkdownTableInMarkdown = (table: MarkdownTable, markdown: string, beginLineNumber: number, endLineNumber: number): string => {
+  const splitMarkdown = markdown.split(/\r\n|\r|\n/);
+  const markdownBeforeTable = splitMarkdown.slice(0, beginLineNumber - 1);
+  const markdownAfterTable = splitMarkdown.slice(endLineNumber);
+
+  let newMarkdown = '';
+  if (markdownBeforeTable.length > 0) {
+    newMarkdown += `${markdownBeforeTable.join('\n')}\n`;
+  }
+  newMarkdown += table;
+  if (markdownAfterTable.length > 0) {
+    newMarkdown += `\n${markdownAfterTable.join('\n')}`;
+  }
+
+  return newMarkdown;
+};

+ 3 - 7
apps/app/src/stores/modal.tsx

@@ -507,8 +507,6 @@ type HandsonTableModalSaveHandler = (table: MarkdownTable) => void;
 type HandsontableModalStatus = {
   isOpened: boolean,
   table: MarkdownTable,
-  // TODO: Define editor type
-  editor?: any,
   autoFormatMarkdownTable?: boolean,
   // onSave is passed only when editing table directly from the page.
   onSave?: HandsonTableModalSaveHandler
@@ -517,7 +515,6 @@ type HandsontableModalStatus = {
 type HandsontableModalStatusUtils = {
   open(
     table: MarkdownTable,
-    editor?: any,
     autoFormatMarkdownTable?: boolean,
     onSave?: HandsonTableModalSaveHandler
   ): void
@@ -541,7 +538,6 @@ export const useHandsontableModal = (status?: HandsontableModalStatus): SWRRespo
   const initialData: HandsontableModalStatus = {
     isOpened: false,
     table: defaultMarkdownTable(),
-    editor: undefined,
     autoFormatMarkdownTable: false,
   };
 
@@ -549,14 +545,14 @@ export const useHandsontableModal = (status?: HandsontableModalStatus): SWRRespo
 
   const { mutate } = swrResponse;
 
-  const open = useCallback((table: MarkdownTable, editor?: any, autoFormatMarkdownTable?: boolean, onSave?: HandsonTableModalSaveHandler): void => {
+  const open = useCallback((table: MarkdownTable, autoFormatMarkdownTable?: boolean, onSave?: HandsonTableModalSaveHandler): void => {
     mutate({
-      isOpened: true, table, editor, autoFormatMarkdownTable, onSave,
+      isOpened: true, table, autoFormatMarkdownTable, onSave,
     });
   }, [mutate]);
   const close = useCallback((): void => {
     mutate({
-      isOpened: false, table: defaultMarkdownTable(), editor: undefined, autoFormatMarkdownTable: false, onSave: undefined,
+      isOpened: false, table: defaultMarkdownTable(), autoFormatMarkdownTable: false, onSave: undefined,
     });
   }, [mutate]);
 

+ 2 - 2
packages/editor/src/components/CodeMirrorEditor/CodeMirrorEditor.tsx

@@ -139,12 +139,12 @@ export const CodeMirrorEditor = (props: Props): JSX.Element => {
     }
 
     return '';
-  }, [isUploading, isDragAccept,isDragReject, acceptedFileType]);
+  }, [isUploading, isDragAccept, isDragReject, acceptedFileType]);
 
   return (
     <div className={`${style['codemirror-editor']} flex-expand-vert`}>
       <div {...getRootProps()} className={`dropzone ${fileUploadState} flex-expand-vert`}>
-        <FileDropzoneOverlay isEnabled={isDragActive}/>
+        <FileDropzoneOverlay isEnabled={isDragActive} />
         <CodeMirrorEditorContainer ref={containerRef} />
         <Toolbar editorKey={editorKey} onFileOpen={open} acceptedFileType={acceptedFileType} />
       </div>

+ 19 - 2
packages/editor/src/components/CodeMirrorEditor/Toolbar/TableButton.tsx

@@ -1,6 +1,23 @@
-export const TableButton = (): JSX.Element => {
+import { useCallback } from 'react';
+
+import { useCodeMirrorEditorIsolated } from '../../../stores';
+import { useHandsontableModalForEditor } from '../../../stores/use-handsontable';
+
+type Props = {
+  editorKey: string,
+}
+
+export const TableButton = (props: Props): JSX.Element => {
+  const { editorKey } = props;
+  const { data: codeMirrorEditor } = useCodeMirrorEditorIsolated(editorKey);
+  const { open: openTableModal } = useHandsontableModalForEditor();
+  const editor = codeMirrorEditor?.view;
+  const onClickTableButton = useCallback(() => {
+    openTableModal(editor);
+  }, [editor, openTableModal]);
+
   return (
-    <button type="button" className="btn btn-toolbar-button">
+    <button type="button" className="btn btn-toolbar-button" onClick={onClickTableButton}>
       <span className="material-symbols-outlined fs-5">table_chart</span>
     </button>
   );

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

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

+ 45 - 0
packages/editor/src/stores/use-handsontable.ts

@@ -0,0 +1,45 @@
+import { useCallback } from 'react';
+
+import { EditorView } from '@codemirror/view';
+import { useSWRStatic } from '@growi/core/dist/swr';
+import type { SWRResponse } from 'swr';
+
+type HandsontableModalStatus = {
+  isOpened: boolean,
+  editor?: EditorView,
+}
+
+type HandsontableModalStatusUtils = {
+  open(
+    editor?: EditorView,
+  ): void
+  close(): void
+}
+
+export const useHandsontableModalForEditor = (status?: HandsontableModalStatus): SWRResponse<HandsontableModalStatus, Error> & HandsontableModalStatusUtils => {
+  const initialData: HandsontableModalStatus = {
+    isOpened: false,
+    editor: undefined,
+  };
+
+  const swrResponse = useSWRStatic<HandsontableModalStatus, Error>('handsontableModalStatus', status, { fallbackData: initialData });
+
+  const { mutate } = swrResponse;
+
+  const open = useCallback((editor?: EditorView): void => {
+    mutate({
+      isOpened: true, editor,
+    });
+  }, [mutate]);
+  const close = useCallback((): void => {
+    mutate({
+      isOpened: false, editor: undefined,
+    });
+  }, [mutate]);
+
+  return {
+    ...swrResponse,
+    open,
+    close,
+  };
+};