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 2024/04/15 08:39:15 UTC
(incubator-answer) 02/02: refactor(ui): Refactor CodeMirror toolbar components and update dependencies
This is an automated email from the ASF dual-hosted git repository.
robin0716 pushed a commit to branch refactor/editor
in repository https://gitbox.apache.org/repos/asf/incubator-answer.git
commit 122a5827b92b5dca20d7dab2f1848d454c4fad67
Author: robin <ro...@rnode.me>
AuthorDate: Mon Apr 15 16:39:00 2024 +0800
refactor(ui): Refactor CodeMirror toolbar components and update dependencies
---
ui/src/components/Editor/ToolBars/formula.tsx | 106 ----------------
ui/src/components/Editor/ToolBars/image.tsx | 8 +-
ui/src/components/Editor/ToolBars/index.ts | 2 -
ui/src/components/Editor/types.ts | 47 ++++++-
ui/src/components/Editor/utils/index.ts | 173 ++++++++++++--------------
5 files changed, 129 insertions(+), 207 deletions(-)
diff --git a/ui/src/components/Editor/ToolBars/formula.tsx b/ui/src/components/Editor/ToolBars/formula.tsx
deleted file mode 100644
index 9b273e87..00000000
--- a/ui/src/components/Editor/ToolBars/formula.tsx
+++ /dev/null
@@ -1,106 +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, useState, memo } from 'react';
-import { Dropdown } from 'react-bootstrap';
-import { useTranslation } from 'react-i18next';
-
-import ToolItem from '../toolItem';
-import { IEditorContext } from '../types';
-
-const Formula: FC<IEditorContext> = ({ editor, wrapText }) => {
- const { t } = useTranslation('translation', { keyPrefix: 'editor' });
- const formulaList = [
- {
- type: 'line',
- label: t('formula.options.inline'),
- },
- {
- type: 'block',
- label: t('formula.options.block'),
- },
- ];
- const item = {
- label: 'formula',
- tip: t('formula.text'),
- };
- const [isShow, setShowState] = useState(false);
- const [isLocked, setLockState] = useState(false);
-
- const handleClick = (type, label) => {
- if (!editor) {
- return;
- }
- if (type === 'line') {
- wrapText('\\\\( ', ' \\\\)', label);
- } else {
- const cursor = editor.getCursor();
-
- wrapText('\n$$\n', '\n$$\n', label);
-
- editor.setSelection(
- { line: cursor.line + 2, ch: 0 },
- { line: cursor.line + 2, ch: label.length },
- );
- }
- editor?.focus();
- setShowState(false);
- };
- const onAddFormula = () => {
- if (isLocked) {
- return;
- }
- setShowState(!isShow);
- };
-
- const handleMouseEnter = () => {
- setLockState(true);
- };
-
- const handleMouseLeave = () => {
- setLockState(false);
- };
- return (
- <ToolItem
- as="dropdown"
- {...item}
- isShow={isShow}
- onClick={onAddFormula}
- onBlur={onAddFormula}>
- <Dropdown.Menu
- onMouseEnter={handleMouseEnter}
- onMouseLeave={handleMouseLeave}>
- {formulaList.map((formula) => {
- return (
- <Dropdown.Item
- key={formula.label}
- onClick={(e) => {
- e.preventDefault();
- handleClick(formula.type, formula.label);
- }}>
- {formula.label}
- </Dropdown.Item>
- );
- })}
- </Dropdown.Menu>
- </ToolItem>
- );
-};
-
-export default memo(Formula);
diff --git a/ui/src/components/Editor/ToolBars/image.tsx b/ui/src/components/Editor/ToolBars/image.tsx
index d007408f..60099193 100644
--- a/ui/src/components/Editor/ToolBars/image.tsx
+++ b/ui/src/components/Editor/ToolBars/image.tsx
@@ -21,20 +21,16 @@ import { useEffect, useState, memo } from 'react';
import { Button, Form, Modal, Tab, Tabs } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
-import { EditorView } from '@codemirror/view';
import { EditorState, StateEffect } from '@codemirror/state';
import { Modal as AnswerModal } from '@/components';
import ToolItem from '../toolItem';
-import { IEditorContext } from '../types';
+import { IEditorContext, Editor } from '../types';
import { uploadImage } from '@/services';
-import { ExtendEditor } from '../utils';
let context: IEditorContext;
const Image = ({ editorInstance }) => {
- const [editor, setEditor] = useState<EditorView & ExtendEditor>(
- editorInstance,
- );
+ const [editor, setEditor] = useState<Editor>(editorInstance);
const { t } = useTranslation('translation', { keyPrefix: 'editor' });
const loadingText = `![${t('image.uploading')}...]()`;
diff --git a/ui/src/components/Editor/ToolBars/index.ts b/ui/src/components/Editor/ToolBars/index.ts
index f3bc8209..ce04587d 100644
--- a/ui/src/components/Editor/ToolBars/index.ts
+++ b/ui/src/components/Editor/ToolBars/index.ts
@@ -18,7 +18,6 @@
*/
import Table from './table';
-import Formula from './formula';
import OL from './ol';
import UL from './ul';
import Indent from './indent';
@@ -36,7 +35,6 @@ import Chart from './chart';
export {
Table,
- Formula,
OL,
UL,
Indent,
diff --git a/ui/src/components/Editor/types.ts b/ui/src/components/Editor/types.ts
index ebc7dbca..be4d8511 100644
--- a/ui/src/components/Editor/types.ts
+++ b/ui/src/components/Editor/types.ts
@@ -16,9 +16,54 @@
* specific language governing permissions and limitations
* under the License.
*/
+import { EditorView, Command } from '@codemirror/view';
-import type { Editor } from 'codemirror';
+export interface Position {
+ ch: number;
+ line: number;
+ sticky?: string | undefined;
+}
+export interface ExtendEditor {
+ addKeyMap: (keyMap: Record<string, Command>) => void;
+ on: (
+ event:
+ | 'change'
+ | 'focus'
+ | 'blur'
+ | 'dragenter'
+ | 'dragover'
+ | 'drop'
+ | 'paste',
+ callback: (e?) => void,
+ ) => void;
+ getValue: () => string;
+ setValue: (value: string) => void;
+ off: (
+ event:
+ | 'change'
+ | 'focus'
+ | 'blur'
+ | 'dragenter'
+ | 'dragover'
+ | 'drop'
+ | 'paste',
+ callback: (e?) => void,
+ ) => void;
+ getSelection: () => string;
+ replaceSelection: (value: string) => void;
+ focus: () => void;
+ wrapText: (before: string, after?: string, defaultText?: string) => void;
+ replaceLines: (
+ replace: Parameters<Array<string>['map']>[0],
+ symbolLen?: number,
+ ) => void;
+ appendBlock: (content: string) => void;
+ getCursor: () => Position;
+ replaceRange: (value: string, from: Position, to: Position) => void;
+ setSelection: (anchor: Position, head?: Position) => void;
+}
+export type Editor = EditorView & ExtendEditor;
export interface CodeMirrorEditor extends Editor {
display: any;
diff --git a/ui/src/components/Editor/utils/index.ts b/ui/src/components/Editor/utils/index.ts
index 9c65a923..92fcbae3 100644
--- a/ui/src/components/Editor/utils/index.ts
+++ b/ui/src/components/Editor/utils/index.ts
@@ -1,4 +1,3 @@
-/* eslint-disable */
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
@@ -20,18 +19,13 @@
import { useEffect, useState } from 'react';
-import type { Position } from 'codemirror';
+import { minimalSetup } from 'codemirror';
import { EditorSelection, EditorState, StateEffect } from '@codemirror/state';
-import {
- EditorView,
- keymap,
- KeyBinding,
- Command,
- placeholder,
-} from '@codemirror/view';
-import { markdown } from '@codemirror/lang-markdown';
-import type CodeMirror from 'codemirror';
-import 'codemirror/lib/codemirror.css';
+import { EditorView, keymap, KeyBinding, placeholder } from '@codemirror/view';
+import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
+import { languages } from '@codemirror/language-data';
+
+import { Editor, Position } from '../types';
export function htmlRender(el: HTMLElement | null) {
if (!el) return;
@@ -72,46 +66,8 @@ export function htmlRender(el: HTMLElement | null) {
}
});
}
-export interface ExtendEditor {
- addKeyMap: (keyMap: Record<string, Command>) => void;
- on: (
- event:
- | 'change'
- | 'focus'
- | 'blur'
- | 'dragenter'
- | 'dragover'
- | 'drop'
- | 'paste',
- callback: (e?) => void,
- ) => void;
- getValue: () => string;
- setValue: (value: string) => void;
- off: (
- event:
- | 'change'
- | 'focus'
- | 'blur'
- | 'dragenter'
- | 'dragover'
- | 'drop'
- | 'paste',
- callback: (e?) => void,
- ) => void;
- getSelection: () => string;
- replaceSelection: (value: string) => void;
- focus: () => void;
- wrapText: (before: string, after?: string, defaultText?: string) => void;
- replaceLines: (
- replace: Parameters<Array<string>['map']>[0],
- symbolLen?: number,
- ) => void;
- appendBlock: (content: string) => Position;
- getCursor: () => Position;
- replaceRange: (value: string, from: Position, to: Position) => void;
-}
-const createEditorUtils = (editor: EditorView & ExtendEditor) => {
+const createEditorUtils = (editor: Editor) => {
editor.focus = () => {
editor.contentDOM.focus();
};
@@ -119,9 +75,8 @@ const createEditorUtils = (editor: EditorView & ExtendEditor) => {
editor.getCursor = () => {
const range = editor.state.selection.ranges[0];
const line = editor.state.doc.lineAt(range.from).number;
- const from = editor.state.doc.line(line).from;
- const to = editor.state.doc.line(line).to;
- return { from, to, ch: range.from - from };
+ const { from, to } = editor.state.doc.line(line);
+ return { from, to, ch: range.from - from, line };
};
editor.addKeyMap = (keyMap) => {
@@ -161,6 +116,19 @@ const createEditorUtils = (editor: EditorView & ExtendEditor) => {
});
};
+ editor.setSelection = (anchor: Position, head?: Position) => {
+ editor.dispatch({
+ selection: EditorSelection.create([
+ EditorSelection.range(
+ editor.state.doc.line(anchor.line).from + anchor.ch,
+ head
+ ? editor.state.doc.line(head.line).from + head.ch
+ : editor.state.doc.line(anchor.line).from + anchor.ch,
+ ),
+ ]),
+ });
+ };
+
editor.on = (event, callback) => {
if (event === 'change') {
const change = EditorView.updateListener.of((update) => {
@@ -242,11 +210,13 @@ const createEditorUtils = (editor: EditorView & ExtendEditor) => {
editor.replaceSelection(`${before}${defaultText}${after}`);
}
};
- editor.replaceLines = (replace: Parameters<Array<string>['map']>[0]) => {
+ editor.replaceLines = (
+ replace: Parameters<Array<string>['map']>[0],
+ symbolLen = 0,
+ ) => {
const range = editor.state.selection.ranges[0];
const line = editor.state.doc.lineAt(range.from).number;
- const from = editor.state.doc.line(line).from;
- const to = editor.state.doc.line(line).to;
+ const { from, to } = editor.state.doc.line(line);
const lines = editor.state.sliceDoc(from, to).split('\n');
const insert = lines.map(replace).join('\n');
@@ -261,20 +231,25 @@ const createEditorUtils = (editor: EditorView & ExtendEditor) => {
insert,
},
],
- selection: EditorSelection.single(selectionStart, selectionEnd),
+ selection: EditorSelection.create([
+ EditorSelection.range(selectionStart + symbolLen, selectionEnd),
+ ]),
});
};
- editor.appendBlock = (content: string): Position => {
+ editor.appendBlock = (content: string) => {
const range = editor.state.selection.ranges[0];
const line = editor.state.doc.lineAt(range.from).number;
- const from = editor.state.doc.line(line).from;
- const to = editor.state.doc.line(line).to;
+ const { from, to } = editor.state.doc.line(line);
+
let insert = `\n\n${content}`;
+
let selection = EditorSelection.single(to, to + content.length);
if (from === to) {
insert = `${content}\n`;
- selection = EditorSelection.single(from, from + content.length);
+ selection = EditorSelection.create([
+ EditorSelection.cursor(to + content.length),
+ ]);
}
editor.dispatch({
@@ -293,8 +268,9 @@ const createEditorUtils = (editor: EditorView & ExtendEditor) => {
selectionStart: Position,
selectionEnd: Position,
) => {
- const from = selectionStart.from;
- const to = selectionEnd.to + selectionEnd.ch;
+ const from =
+ editor.state.doc.line(selectionStart.line).from + selectionStart.ch;
+ const to = editor.state.doc.line(selectionEnd.line).from + selectionEnd.ch;
editor.dispatch({
changes: [
{
@@ -309,6 +285,7 @@ const createEditorUtils = (editor: EditorView & ExtendEditor) => {
return editor;
};
+
export const useEditor = ({
editorRef,
placeholder: placeholderText,
@@ -317,29 +294,41 @@ export const useEditor = ({
onFocus,
onBlur,
}) => {
- const [editor, setEditor] = useState<CodeMirror.Editor | null>(null);
+ const [editor, setEditor] = useState<Editor | null>(null);
const [value, setValue] = useState<string>('');
const init = async () => {
- const theme = EditorView.theme({
- '&': {
- width: '100%',
- height: '100%',
- },
- '&.cm-focused': {
- outline: 'none',
- },
- '.cm-content': {
- width: '100%',
- padding: '1rem',
- },
- '.cm-line': {
- whiteSpace: 'pre-wrap',
- wordWrap: 'break-word',
- wordBreak: 'break-all',
+ const theme = EditorView.theme(
+ {
+ '&': {
+ width: '100%',
+ height: '100%',
+ },
+ '&.cm-focused': {
+ outline: 'none',
+ },
+ '.cm-content': {
+ width: '100%',
+ padding: '1rem',
+ },
+ '.cm-line': {
+ whiteSpace: 'pre-wrap',
+ wordWrap: 'break-word',
+ wordBreak: 'break-all',
+ },
},
- });
- let startState = EditorState.create({
- extensions: [markdown(), theme, placeholder(placeholderText)],
+ { dark: false },
+ );
+
+ const startState = EditorState.create({
+ extensions: [
+ minimalSetup,
+ markdown({
+ codeLanguages: languages,
+ base: markdownLanguage,
+ }),
+ theme,
+ placeholder(placeholderText),
+ ],
});
const view = new EditorView({
@@ -347,30 +336,30 @@ export const useEditor = ({
state: startState,
});
- const editor = createEditorUtils(view as EditorView & ExtendEditor);
+ const cm = createEditorUtils(view as Editor);
if (autoFocus) {
setTimeout(() => {
- editor.focus();
+ cm.focus();
}, 10);
}
- editor.on('change', () => {
- const newValue = editor.getValue();
+ cm.on('change', () => {
+ const newValue = cm.getValue();
setValue(newValue);
});
- editor.on('focus', () => {
+ cm.on('focus', () => {
onFocus?.();
});
- editor.on('blur', () => {
+ cm.on('blur', () => {
onBlur?.();
});
- setEditor(editor);
+ setEditor(cm);
- return editor;
+ return cm;
};
useEffect(() => {