Shun Miyazawa 4 месяцев назад
Родитель
Сommit
737d0f6292

+ 128 - 87
apps/app/src/client/components/PageEditor/HandsontableModal/HandsontableModal.tsx

@@ -1,5 +1,5 @@
 import React, {
-  useState, useCallback, useMemo, type JSX,
+  useState, useCallback, useMemo, useEffect, type JSX,
 } from 'react';
 
 import { MarkdownTable, useHandsontableModalForEditorStatus, useHandsontableModalForEditorActions } from '@growi/editor';
@@ -12,7 +12,6 @@ import {
 } from 'reactstrap';
 import { debounce } from 'throttle-debounce';
 
-
 import { replaceFocusedMarkdownTableWithEditor, getMarkdownTable } from '~/client/components/PageEditor/markdown-table-util-for-editor';
 import { useHandsontableModalActions, useHandsontableModalStatus } from '~/states/ui/modal/handsontable';
 
@@ -30,25 +29,33 @@ const MARKDOWNTABLE_TO_HANDSONTABLE_ALIGNMENT_SYMBOL_MAPPING = {
   '': '',
 };
 
-const HandsontableModalSubstance = (): JSX.Element => {
-  const { t } = useTranslation('commons');
-
-  // for View
-  const { close: closeHandsontableModal } = useHandsontableModalActions();
-  const handsontableModalData = useHandsontableModalStatus();
-  const isOpened = handsontableModalData.isOpened;
-  const table = handsontableModalData?.table;
-  const autoFormatMarkdownTable = handsontableModalData?.autoFormatMarkdownTable ?? false;
-  const onSave = handsontableModalData?.onSave;
-
-  // for Editor
-  const { close: closeHandsontableModalForEditor } = useHandsontableModalForEditorActions();
-  const handsontableModalForEditorData = useHandsontableModalForEditorStatus();
-  const isOpendInEditor = handsontableModalForEditorData.isOpened;
-  const editor = handsontableModalForEditorData?.editor;
+type HandsontableModalSubstanceProps = {
+  initialTable: MarkdownTable | undefined;
+  autoFormatMarkdownTable: boolean;
+  isWindowExpanded: boolean;
+  onSave: (table: MarkdownTable) => void;
+  onCancel: () => void;
+  expandWindow: () => void;
+  contractWindow: () => void;
+};
 
+/**
+ * HandsontableModalSubstance - Presentation component (heavy logic, rendered only when isOpen)
+ */
+const HandsontableModalSubstance = (props: HandsontableModalSubstanceProps): JSX.Element => {
+  const {
+    initialTable,
+    autoFormatMarkdownTable,
+    isWindowExpanded,
+
+    // Handlers
+    onSave,
+    onCancel,
+    expandWindow,
+    contractWindow,
+  } = props;
 
-  // Memoize default table creation
+  const { t } = useTranslation('commons');
   const defaultMarkdownTable = useMemo(() => {
     return new MarkdownTable(
       [
@@ -94,7 +101,6 @@ const HandsontableModalSubstance = (): JSX.Element => {
   const [hotTable, setHotTable] = useState<HotTable | null>();
   const [hotTableContainer, setHotTableContainer] = useState<HTMLDivElement | null>();
   const [isDataImportAreaExpanded, setIsDataImportAreaExpanded] = useState<boolean>(false);
-  const [isWindowExpanded, setIsWindowExpanded] = useState<boolean>(false);
   const [markdownTable, setMarkdownTable] = useState<MarkdownTable>(defaultMarkdownTable);
   const [markdownTableOnInit, setMarkdownTableOnInit] = useState<MarkdownTable>(defaultMarkdownTable);
   const [handsontableHeight, setHandsontableHeight] = useState<number>(DEFAULT_HOT_HEIGHT);
@@ -115,42 +121,22 @@ const HandsontableModalSubstance = (): JSX.Element => {
     debounce(100, handleWindowExpandedChange)
   ), [handleWindowExpandedChange]);
 
-  // Memoize modal open handler
-  const handleModalOpen = useCallback(() => {
-    let markdownTableState: MarkdownTable | undefined;
-    if (isOpendInEditor) {
-      markdownTableState = editor != null ? getMarkdownTable(editor) : undefined;
-    }
-    else {
-      markdownTableState = table;
-    }
-    const initTableInstance = markdownTableState == null ? defaultMarkdownTable : markdownTableState.clone();
-    setMarkdownTable(markdownTableState ?? defaultMarkdownTable);
+  // Initialize table data when component mounts (modal opens)
+  useEffect(() => {
+    const initTableInstance = initialTable == null ? defaultMarkdownTable : initialTable.clone();
+    setMarkdownTable(initialTable ?? defaultMarkdownTable);
     setMarkdownTableOnInit(initTableInstance);
     debouncedHandleWindowExpandedChange();
-  }, [isOpendInEditor, defaultMarkdownTable, debouncedHandleWindowExpandedChange, editor, table]);
-
-  // Memoize expand/contract handlers
-  const expandWindow = useCallback(() => {
-    setIsWindowExpanded(true);
-    debouncedHandleWindowExpandedChange();
-  }, [debouncedHandleWindowExpandedChange]);
+  }, [debouncedHandleWindowExpandedChange, defaultMarkdownTable, initialTable]); // Run only on mount
 
-  const contractWindow = useCallback(() => {
-    setIsWindowExpanded(false);
-    // Set the height to the default value
-    setHandsontableHeight(DEFAULT_HOT_HEIGHT);
+  // Update handsontable size when window expansion changes
+  useEffect(() => {
+    if (!isWindowExpanded) {
+      // Reset height to default when contracted
+      setHandsontableHeight(DEFAULT_HOT_HEIGHT);
+    }
     debouncedHandleWindowExpandedChange();
-  }, [debouncedHandleWindowExpandedChange]);
-
-  const markdownTableOption = {
-    get latest() {
-      return {
-        align: [].concat(markdownTable.options.align),
-        pad: autoFormatMarkdownTable !== false,
-      };
-    },
-  };
+  }, [isWindowExpanded, debouncedHandleWindowExpandedChange]);
 
   /**
    * Reset table data to initial value
@@ -163,36 +149,30 @@ const HandsontableModalSubstance = (): JSX.Element => {
     setMarkdownTable(markdownTableOnInit.clone());
   };
 
-  const cancel = () => {
-    closeHandsontableModal();
-    closeHandsontableModalForEditor();
+  const cancel = useCallback(() => {
     setIsDataImportAreaExpanded(false);
-    setIsWindowExpanded(false);
-  };
+    contractWindow();
+    onCancel();
+  }, [contractWindow, onCancel]);
 
-  const save = () => {
+  const save = useCallback(() => {
     if (hotTable == null) {
       return;
     }
 
+    const markdownTableOption = {
+      align: [].concat(markdownTable.options.align),
+      pad: autoFormatMarkdownTable !== false,
+    };
+
     const newMarkdownTable = new MarkdownTable(
       hotTable.hotInstance.getData(),
-      markdownTableOption.latest,
+      markdownTableOption,
     ).normalizeCells();
 
-    // onSave is passed only when editing table directly from the page.
-    if (onSave != null) {
-      onSave(newMarkdownTable);
-      cancel();
-      return;
-    }
-
-    if (editor == null) {
-      return;
-    }
-    replaceFocusedMarkdownTableWithEditor(editor, newMarkdownTable);
+    onSave(newMarkdownTable);
     cancel();
-  };
+  }, [hotTable, markdownTable.options.align, autoFormatMarkdownTable, onSave, cancel]);
 
   const beforeColumnResizeHandler = (currentColumn) => {
     /*
@@ -467,16 +447,7 @@ const HandsontableModalSubstance = (): JSX.Element => {
   );
 
   return (
-    <Modal
-      isOpen={isOpened || isOpendInEditor}
-      toggle={cancel}
-      backdrop="static"
-      keyboard={false}
-      size="lg"
-      wrapClassName={`${styles['grw-handsontable']}`}
-      className={`handsontable-modal ${isWindowExpanded && 'grw-modal-expanded'}`}
-      onOpened={handleModalOpen}
-    >
+    <>
       <ModalHeader tag="h4" toggle={cancel} close={closeButton}>
         {t('handsontable_modal.title')}
       </ModalHeader>
@@ -540,19 +511,89 @@ const HandsontableModalSubstance = (): JSX.Element => {
           <button type="button" className="btn btn-primary" onClick={save}>{t('handsontable_modal.done')}</button>
         </div>
       </ModalFooter>
-    </Modal>
+    </>
   );
 };
 
+/**
+ * HandsontableModal - Container component (lightweight, always rendered)
+ * Handles both View and Editor modes
+ */
 export const HandsontableModal = (): JSX.Element => {
+  // for View
   const handsontableModalData = useHandsontableModalStatus();
+  const { close: closeHandsontableModal } = useHandsontableModalActions();
+
+  // for Editor
   const handsontableModalForEditorData = useHandsontableModalForEditorStatus();
+  const { close: closeHandsontableModalForEditor } = useHandsontableModalForEditorActions();
+
+  const isOpenedForView = handsontableModalData.isOpened;
+  const isOpenedForEditor = handsontableModalForEditorData.isOpened;
+  const isOpened = isOpenedForView || isOpenedForEditor;
 
-  const isOpened = (handsontableModalData.isOpened || handsontableModalForEditorData.isOpened);
+  const [isWindowExpanded, setIsWindowExpanded] = useState<boolean>(false);
+
+  // Determine initial table based on mode
+  const initialTable = useMemo(() => {
+    if (isOpenedForEditor) {
+      const editor = handsontableModalForEditorData.editor;
+      return editor != null ? getMarkdownTable(editor) : undefined;
+    }
+    return handsontableModalData.table;
+  }, [isOpenedForEditor, handsontableModalForEditorData.editor, handsontableModalData.table]);
 
-  if (!isOpened) {
-    return <></>;
-  }
+  // Determine autoFormatMarkdownTable based on mode
+  const autoFormatMarkdownTable = handsontableModalData.autoFormatMarkdownTable ?? false;
+
+  const toggle = useCallback(() => {
+    closeHandsontableModal();
+    closeHandsontableModalForEditor();
+    setIsWindowExpanded(false);
+  }, [closeHandsontableModal, closeHandsontableModalForEditor]);
 
-  return <HandsontableModalSubstance />;
+  const expandWindow = useCallback(() => {
+    setIsWindowExpanded(true);
+  }, []);
+
+  const contractWindow = useCallback(() => {
+    setIsWindowExpanded(false);
+  }, []);
+
+  // Create save handler based on mode
+  const handleSave = useCallback((newMarkdownTable: MarkdownTable) => {
+    if (isOpenedForEditor) {
+      const editor = handsontableModalForEditorData.editor;
+      if (editor != null) {
+        replaceFocusedMarkdownTableWithEditor(editor, newMarkdownTable);
+      }
+    }
+    else {
+      handsontableModalData.onSave?.(newMarkdownTable);
+    }
+  }, [isOpenedForEditor, handsontableModalForEditorData.editor, handsontableModalData]);
+
+  return (
+    <Modal
+      isOpen={isOpened}
+      toggle={toggle}
+      backdrop="static"
+      keyboard={false}
+      size="lg"
+      wrapClassName={`${styles['grw-handsontable']}`}
+      className={`handsontable-modal ${isWindowExpanded ? 'grw-modal-expanded' : ''}`}
+    >
+      {isOpened && (
+        <HandsontableModalSubstance
+          initialTable={initialTable}
+          autoFormatMarkdownTable={autoFormatMarkdownTable}
+          onSave={handleSave}
+          onCancel={toggle}
+          isWindowExpanded={isWindowExpanded}
+          expandWindow={expandWindow}
+          contractWindow={contractWindow}
+        />
+      )}
+    </Modal>
+  );
 };