You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@answer.apache.org by ro...@apache.org on 2025/12/25 03:51:58 UTC
(answer) 05/12: refactor(editor): enhance editor components with base props and initialization logic
This is an automated email from the ASF dual-hosted git repository.
robin0716 pushed a commit to branch feat/1.7.2/ui
in repository https://gitbox.apache.org/repos/asf/answer.git
commit d87726bd6e54e4fc88f8338f6f6499f878b00c4f
Author: robin <ro...@rnode.me>
AuthorDate: Wed Dec 17 10:38:31 2025 +0800
refactor(editor): enhance editor components with base props and initialization logic
- Introduced BaseEditorProps interface to standardize props across MarkdownEditor and RichEditor components.
- Improved initialization logic in RichEditor and MarkdownEditor to handle editor state more effectively.
- Updated useEditor hook to support initial values and prevent unnecessary updates during prop changes.
- Refactored command methods to utilize dispatch for state changes in CodeMirror editor.
---
ui/src/components/Editor/MarkdownEditor.tsx | 33 ++--
ui/src/components/Editor/RichEditor.tsx | 137 +++++++++-------
ui/src/components/Editor/ToolBars/chart.tsx | 181 ---------------------
ui/src/components/Editor/ToolBars/index.ts | 2 -
ui/src/components/Editor/index.tsx | 5 +-
ui/src/components/Editor/types.ts | 15 +-
.../components/Editor/utils/codemirror/commands.ts | 16 +-
ui/src/components/Editor/utils/index.ts | 26 ++-
ui/src/components/Editor/utils/tiptap/events.ts | 8 +-
9 files changed, 145 insertions(+), 278 deletions(-)
diff --git a/ui/src/components/Editor/MarkdownEditor.tsx b/ui/src/components/Editor/MarkdownEditor.tsx
index fe5d8bb4..068a408f 100644
--- a/ui/src/components/Editor/MarkdownEditor.tsx
+++ b/ui/src/components/Editor/MarkdownEditor.tsx
@@ -21,18 +21,10 @@ import { useEffect, useRef } from 'react';
import { EditorView } from '@codemirror/view';
-import { Editor } from './types';
+import { BaseEditorProps } from './types';
import { useEditor } from './utils';
-interface MarkdownEditorProps {
- value: string;
- onChange?: (value: string) => void;
- onFocus?: () => void;
- onBlur?: () => void;
- placeholder?: string;
- autoFocus?: boolean;
- onEditorReady?: (editor: Editor) => void;
-}
+interface MarkdownEditorProps extends BaseEditorProps {}
const MarkdownEditor: React.FC<MarkdownEditorProps> = ({
value,
@@ -45,6 +37,7 @@ const MarkdownEditor: React.FC<MarkdownEditorProps> = ({
}) => {
const editorRef = useRef<HTMLDivElement>(null);
const lastSyncedValueRef = useRef<string>(value);
+ const isInitializedRef = useRef<boolean>(false);
const editor = useEditor({
editorRef,
@@ -53,24 +46,20 @@ const MarkdownEditor: React.FC<MarkdownEditorProps> = ({
onBlur,
placeholder,
autoFocus,
+ initialValue: value,
});
useEffect(() => {
- if (!editor) {
+ if (!editor || isInitializedRef.current) {
return;
}
- editor.setValue(value || '');
- lastSyncedValueRef.current = value || '';
+ isInitializedRef.current = true;
onEditorReady?.(editor);
- }, [editor]);
+ }, [editor, onEditorReady]);
useEffect(() => {
- if (!editor) {
- return;
- }
-
- if (value === lastSyncedValueRef.current) {
+ if (!editor || value === lastSyncedValueRef.current) {
return;
}
@@ -82,6 +71,9 @@ const MarkdownEditor: React.FC<MarkdownEditorProps> = ({
}, [editor, value]);
useEffect(() => {
+ lastSyncedValueRef.current = value;
+ isInitializedRef.current = false;
+
return () => {
if (editor) {
const view = editor as unknown as EditorView;
@@ -89,8 +81,9 @@ const MarkdownEditor: React.FC<MarkdownEditorProps> = ({
view.destroy();
}
}
+ isInitializedRef.current = false;
};
- }, [editor]);
+ }, []);
return (
<div className="content-wrap">
diff --git a/ui/src/components/Editor/RichEditor.tsx b/ui/src/components/Editor/RichEditor.tsx
index c574551c..916c8249 100644
--- a/ui/src/components/Editor/RichEditor.tsx
+++ b/ui/src/components/Editor/RichEditor.tsx
@@ -30,18 +30,10 @@ import { Markdown } from '@tiptap/markdown';
import Image from '@tiptap/extension-image';
import { TableKit } from '@tiptap/extension-table';
-import { Editor } from './types';
+import { Editor, BaseEditorProps } from './types';
import { createTipTapAdapter } from './utils/tiptap/adapter';
-interface RichEditorProps {
- value: string;
- onChange?: (value: string) => void;
- onFocus?: () => void;
- onBlur?: () => void;
- placeholder?: string;
- autoFocus?: boolean;
- onEditorReady?: (editor: Editor) => void;
-}
+interface RichEditorProps extends BaseEditorProps {}
const RichEditor: React.FC<RichEditorProps> = ({
value,
@@ -53,14 +45,60 @@ const RichEditor: React.FC<RichEditorProps> = ({
onEditorReady,
}) => {
const lastSyncedValueRef = useRef<string>(value);
-
const adaptedEditorRef = useRef<Editor | null>(null);
+ const isInitializedRef = useRef<boolean>(false);
+ const isUpdatingFromPropsRef = useRef<boolean>(false);
+ const onEditorReadyRef = useRef(onEditorReady);
+ const autoFocusRef = useRef(autoFocus);
+ const initialValueRef = useRef<string>(value);
+
+ useEffect(() => {
+ onEditorReadyRef.current = onEditorReady;
+ autoFocusRef.current = autoFocus;
+ }, [onEditorReady, autoFocus]);
+
+ const isViewAvailable = (editorInstance: TipTapEditor | null): boolean => {
+ if (!editorInstance) {
+ return false;
+ }
+ if (editorInstance.isDestroyed) {
+ return false;
+ }
+ return !!(editorInstance.view && editorInstance.state);
+ };
+
+ const handleCreate = useCallback(
+ ({ editor: editorInstance }: { editor: TipTapEditor }) => {
+ if (isInitializedRef.current || !isViewAvailable(editorInstance)) {
+ return;
+ }
+
+ isInitializedRef.current = true;
+
+ const initialValue = initialValueRef.current;
+ if (initialValue && initialValue.trim() !== '') {
+ editorInstance.commands.setContent(initialValue, {
+ contentType: 'markdown',
+ });
+ lastSyncedValueRef.current = initialValue;
+ }
+
+ adaptedEditorRef.current = createTipTapAdapter(editorInstance);
+ onEditorReadyRef.current?.(adaptedEditorRef.current);
+
+ if (autoFocusRef.current) {
+ editorInstance.commands.focus();
+ }
+ },
+ [],
+ );
const handleUpdate = useCallback(
({ editor: editorInstance }: { editor: TipTapEditor }) => {
- if (onChange) {
+ if (onChange && !isUpdatingFromPropsRef.current) {
const markdown = editorInstance.getMarkdown();
onChange(markdown);
+ lastSyncedValueRef.current = markdown;
}
},
[onChange],
@@ -84,7 +122,7 @@ const RichEditor: React.FC<RichEditorProps> = ({
placeholder,
}),
],
- content: value || '',
+ onCreate: handleCreate,
onUpdate: handleUpdate,
onFocus: handleFocus,
onBlur: handleBlur,
@@ -96,67 +134,56 @@ const RichEditor: React.FC<RichEditorProps> = ({
});
useEffect(() => {
- if (!editor) {
+ if (
+ !editor ||
+ !isInitializedRef.current ||
+ !isViewAvailable(editor) ||
+ value === lastSyncedValueRef.current
+ ) {
return;
}
- const checkEditorReady = () => {
- if (editor.view && editor.view.dom) {
+ try {
+ const currentMarkdown = editor.getMarkdown();
+ if (currentMarkdown !== value) {
+ isUpdatingFromPropsRef.current = true;
if (value && value.trim() !== '') {
editor.commands.setContent(value, { contentType: 'markdown' });
} else {
editor.commands.clearContent();
}
lastSyncedValueRef.current = value || '';
- if (!adaptedEditorRef.current) {
- adaptedEditorRef.current = createTipTapAdapter(editor);
- }
- onEditorReady?.(adaptedEditorRef.current);
- } else {
- setTimeout(checkEditorReady, 10);
+ setTimeout(() => {
+ isUpdatingFromPropsRef.current = false;
+ }, 0);
}
- };
-
- checkEditorReady();
- }, [editor]);
-
- useEffect(() => {
- if (!editor) {
- return;
- }
-
- if (value === lastSyncedValueRef.current) {
- return;
- }
-
- const currentMarkdown = editor.getMarkdown();
- if (currentMarkdown !== value) {
- if (value && value.trim() !== '') {
- editor.commands.setContent(value, { contentType: 'markdown' });
- } else {
- editor.commands.clearContent();
- }
- lastSyncedValueRef.current = value || '';
+ } catch (error) {
+ console.warn('Editor view not available when syncing value:', error);
}
}, [editor, value]);
useEffect(() => {
- if (editor && autoFocus) {
- setTimeout(() => {
- editor.commands.focus();
- }, 100);
- }
- }, [editor, autoFocus]);
+ initialValueRef.current = value;
+ lastSyncedValueRef.current = value;
+ isInitializedRef.current = false;
+ adaptedEditorRef.current = null;
+ isUpdatingFromPropsRef.current = false;
+
+ return () => {
+ if (editor) {
+ editor.destroy();
+ }
+ isInitializedRef.current = false;
+ adaptedEditorRef.current = null;
+ isUpdatingFromPropsRef.current = false;
+ };
+ }, [editor]);
if (!editor) {
return <div className="editor-loading">Loading editor...</div>;
}
- return (
- <div className="rich-editor-wrap">
- <EditorContent editor={editor} />
- </div>
- );
+ return <EditorContent className="rich-editor-wrap" editor={editor} />;
};
export default RichEditor;
diff --git a/ui/src/components/Editor/ToolBars/chart.tsx b/ui/src/components/Editor/ToolBars/chart.tsx
deleted file mode 100644
index a26a5638..00000000
--- a/ui/src/components/Editor/ToolBars/chart.tsx
+++ /dev/null
@@ -1,181 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import { FC, useEffect, useState, memo } from 'react';
-import { Dropdown } from 'react-bootstrap';
-import { useTranslation } from 'react-i18next';
-
-import ToolItem from '../toolItem';
-import { IEditorContext } from '../types';
-
-const Chart: FC<IEditorContext> = ({ editor }) => {
- const { t } = useTranslation('translation', { keyPrefix: 'editor' });
-
- const headerList = [
- {
- label: t('chart.flow_chart'),
- tpl: `graph TD
- A[Christmas] -->|Get money| B(Go shopping)
- B --> C{Let me think}
- C -->|One| D[Laptop]
- C -->|Two| E[iPhone]
- C -->|Three| F[fa:fa-car Car]`,
- },
- {
- label: t('chart.sequence_diagram'),
- tpl: `sequenceDiagram
- Alice->>+John: Hello John, how are you?
- Alice->>+John: John, can you hear me?
- John-->>-Alice: Hi Alice, I can hear you!
- John-->>-Alice: I feel great!
- `,
- },
- {
- label: t('chart.state_diagram'),
- tpl: `stateDiagram-v2
- [*] --> Still
- Still --> [*]
- Still --> Moving
- Moving --> Still
- Moving --> Crash
- Crash --> [*]
- `,
- },
- {
- label: t('chart.class_diagram'),
- tpl: `classDiagram
- Animal <|-- Duck
- Animal <|-- Fish
- Animal <|-- Zebra
- Animal : +int age
- Animal : +String gender
- Animal: +isMammal()
- Animal: +mate()
- class Duck{
- +String beakColor
- +swim()
- +quack()
- }
- class Fish{
- -int sizeInFeet
- -canEat()
- }
- class Zebra{
- +bool is_wild
- +run()
- }
- `,
- },
- {
- label: t('chart.pie_chart'),
- tpl: `pie title Pets adopted by volunteers
- "Dogs" : 386
- "Cats" : 85
- "Rats" : 15
- `,
- },
- {
- label: t('chart.gantt_chart'),
- tpl: `gantt
- title A Gantt Diagram
- dateFormat YYYY-MM-DD
- section Section
- A task :a1, 2014-01-01, 30d
- Another task :after a1 , 20d
- section Another
- Task in sec :2014-01-12 , 12d
- another task : 24d
- `,
- },
- {
- label: t('chart.entity_relationship_diagram'),
- tpl: `erDiagram
- CUSTOMER }|..|{ DELIVERY-ADDRESS : has
- CUSTOMER ||--o{ ORDER : places
- CUSTOMER ||--o{ INVOICE : "liable for"
- DELIVERY-ADDRESS ||--o{ ORDER : receives
- INVOICE ||--|{ ORDER : covers
- ORDER ||--|{ ORDER-ITEM : includes
- PRODUCT-CATEGORY ||--|{ PRODUCT : contains
- PRODUCT ||--o{ ORDER-ITEM : "ordered in"
- `,
- },
- ];
- const item = {
- label: 'chart',
- tip: `${t('chart.text')}`,
- };
- const [isShow, setShowState] = useState(false);
- const [isLocked, setLockState] = useState(false);
-
- useEffect(() => {
- if (!editor) {
- return;
- }
- editor.on('focus', () => {
- setShowState(false);
- });
- }, []);
-
- const click = (tpl) => {
- const { ch } = editor.getCursor();
-
- editor.replaceSelection(`${ch ? '\n' : ''}\`\`\`mermaid\n${tpl}\n\`\`\`\n`);
- };
-
- const onAddHeader = () => {
- setShowState(!isShow);
- };
- const handleMouseEnter = () => {
- if (isLocked) {
- return;
- }
- setLockState(true);
- };
-
- const handleMouseLeave = () => {
- setLockState(false);
- };
- return (
- <ToolItem
- as="dropdown"
- {...item}
- onClick={onAddHeader}
- onBlur={onAddHeader}>
- <Dropdown.Menu
- onMouseEnter={handleMouseEnter}
- onMouseLeave={handleMouseLeave}>
- {headerList.map((header) => {
- return (
- <Dropdown.Item
- key={header.label}
- onClick={(e) => {
- e.preventDefault();
- click(header.tpl);
- }}>
- {header.label}
- </Dropdown.Item>
- );
- })}
- </Dropdown.Menu>
- </ToolItem>
- );
-};
-
-export default memo(Chart);
diff --git a/ui/src/components/Editor/ToolBars/index.ts b/ui/src/components/Editor/ToolBars/index.ts
index 05912bc6..c3fa24be 100644
--- a/ui/src/components/Editor/ToolBars/index.ts
+++ b/ui/src/components/Editor/ToolBars/index.ts
@@ -31,7 +31,6 @@ import Link from './link';
import BlockQuote from './blockquote';
import Image from './image';
import Help from './help';
-import Chart from './chart';
import File from './file';
export {
@@ -49,6 +48,5 @@ export {
BlockQuote,
Image,
Help,
- Chart,
File,
};
diff --git a/ui/src/components/Editor/index.tsx b/ui/src/components/Editor/index.tsx
index c293fa65..eb798c9d 100644
--- a/ui/src/components/Editor/index.tsx
+++ b/ui/src/components/Editor/index.tsx
@@ -98,10 +98,10 @@ const MDEditor: ForwardRefRenderFunction<EditorRef, Props> = (
return;
}
- setMode(newMode);
setCurrentEditor(null);
+ setMode(newMode);
},
- [mode, currentEditor],
+ [mode],
);
const getHtml = useCallback(() => {
@@ -172,6 +172,7 @@ const MDEditor: ForwardRefRenderFunction<EditorRef, Props> = (
</div>
<EditorComponent
+ key={mode}
value={value}
onChange={(markdown) => {
onChange?.(markdown);
diff --git a/ui/src/components/Editor/types.ts b/ui/src/components/Editor/types.ts
index 926d3c20..e7d23081 100644
--- a/ui/src/components/Editor/types.ts
+++ b/ui/src/components/Editor/types.ts
@@ -116,11 +116,12 @@ export interface CodeMirrorEditor extends Editor {
moduleType;
}
-// @deprecated 已废弃,请直接使用 Editor 接口
-// 保留此接口仅用于向后兼容,新代码不应使用
-export interface IEditorContext {
- editor: Editor;
- wrapText?;
- replaceLines?;
- appendBlock?;
+export interface BaseEditorProps {
+ value: string;
+ onChange?: (value: string) => void;
+ onFocus?: () => void;
+ onBlur?: () => void;
+ placeholder?: string;
+ autoFocus?: boolean;
+ onEditorReady?: (editor: Editor) => void;
}
diff --git a/ui/src/components/Editor/utils/codemirror/commands.ts b/ui/src/components/Editor/utils/codemirror/commands.ts
index 152bc632..546382d4 100644
--- a/ui/src/components/Editor/utils/codemirror/commands.ts
+++ b/ui/src/components/Editor/utils/codemirror/commands.ts
@@ -69,14 +69,26 @@ export function createCommandMethods(editor: Editor) {
const newLines = lines.map(replace) as string[];
const newText = newLines.join('\n');
- editor.setValue(newText);
+ editor.dispatch({
+ changes: {
+ from: 0,
+ to: editor.state.doc.length,
+ insert: newText,
+ },
+ });
},
appendBlock: (content: string) => {
const { doc } = editor.state;
const currentText = doc.toString();
const newText = currentText ? `${currentText}\n\n${content}` : content;
- editor.setValue(newText);
+ editor.dispatch({
+ changes: {
+ from: 0,
+ to: editor.state.doc.length,
+ insert: newText,
+ },
+ });
},
insertBold: (text?: string) => {
diff --git a/ui/src/components/Editor/utils/index.ts b/ui/src/components/Editor/utils/index.ts
index 938b3761..5ba7192c 100644
--- a/ui/src/components/Editor/utils/index.ts
+++ b/ui/src/components/Editor/utils/index.ts
@@ -17,7 +17,7 @@
* under the License.
*/
-import { useEffect, useState } from 'react';
+import { useEffect, useState, useRef } from 'react';
import { minimalSetup } from 'codemirror';
import { EditorState, Compartment } from '@codemirror/state';
@@ -129,12 +129,14 @@ export const useEditor = ({
editorRef,
placeholder: placeholderText,
autoFocus,
+ initialValue,
onChange,
onFocus,
onBlur,
}) => {
const [editor, setEditor] = useState<Editor | null>(null);
- const [value, setValue] = useState<string>('');
+ const isInternalUpdateRef = useRef<boolean>(false);
+
const init = async () => {
const isDark = isDarkTheme();
@@ -162,6 +164,7 @@ export const useEditor = ({
});
const startState = EditorState.create({
+ doc: initialValue || '',
extensions: [
minimalSetup,
markdown({
@@ -206,9 +209,20 @@ export const useEditor = ({
}, 10);
}
+ const originalSetValue = cm.setValue;
+ cm.setValue = (newValue: string) => {
+ isInternalUpdateRef.current = true;
+ originalSetValue.call(cm, newValue);
+ setTimeout(() => {
+ isInternalUpdateRef.current = false;
+ }, 0);
+ };
+
cm.on('change', () => {
- const newValue = cm.getValue();
- setValue(newValue);
+ if (!isInternalUpdateRef.current && onChange) {
+ const newValue = cm.getValue();
+ onChange(newValue);
+ }
});
cm.on('focus', () => {
@@ -224,10 +238,6 @@ export const useEditor = ({
return cm;
};
- useEffect(() => {
- onChange?.(value);
- }, [value]);
-
useEffect(() => {
if (!editorRef.current) {
return;
diff --git a/ui/src/components/Editor/utils/tiptap/events.ts b/ui/src/components/Editor/utils/tiptap/events.ts
index 495ee6e5..2a86440d 100644
--- a/ui/src/components/Editor/utils/tiptap/events.ts
+++ b/ui/src/components/Editor/utils/tiptap/events.ts
@@ -25,7 +25,13 @@ import { logWarning } from './errorHandler';
* Checks if editor view is available
*/
function isViewAvailable(editor: TipTapEditor): boolean {
- return !!(editor.view && editor.view.dom);
+ if (!editor) {
+ return false;
+ }
+ if (editor.isDestroyed) {
+ return false;
+ }
+ return !!(editor.view && editor.state);
}
/**