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 <[email protected]> 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(() => {
