|
|
@@ -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>
|
|
|
+ );
|
|
|
};
|