🎨 Modify code block logic
This commit is contained in:
53
frontend/src/views/editor/extensions/codeblock/annotation.ts
Normal file
53
frontend/src/views/editor/extensions/codeblock/annotation.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { Annotation, Transaction } from "@codemirror/state";
|
||||
|
||||
/**
|
||||
* 统一的 CodeBlock 注解,用于标记内部触发的事务。
|
||||
*/
|
||||
export const codeBlockEvent = Annotation.define<string>();
|
||||
|
||||
export const LANGUAGE_CHANGE = "codeblock-language-change";
|
||||
export const ADD_NEW_BLOCK = "codeblock-add-new-block";
|
||||
export const MOVE_BLOCK = "codeblock-move-block";
|
||||
export const DELETE_BLOCK = "codeblock-delete-block";
|
||||
export const CURRENCIES_LOADED = "codeblock-currencies-loaded";
|
||||
|
||||
/**
|
||||
* 统一管理的 userEvent 常量,方便复用与检索。
|
||||
*/
|
||||
export const USER_EVENTS = {
|
||||
INPUT: "input",
|
||||
DELETE: "delete",
|
||||
MOVE: "move",
|
||||
SELECT: "select",
|
||||
DELETE_LINE: "delete.line",
|
||||
DELETE_CUT: "delete.cut",
|
||||
INPUT_PASTE: "input.paste",
|
||||
MOVE_LINE: "move.line",
|
||||
MOVE_CHARACTER: "move.character",
|
||||
SELECT_BLOCK_BOUNDARY: "select.block-boundary",
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 判断事务列表中是否包含指定注解。
|
||||
*/
|
||||
export function transactionsHasAnnotation(
|
||||
transactions: readonly Transaction[],
|
||||
annotation: string
|
||||
) {
|
||||
return transactions.some(
|
||||
tr => tr.annotation(codeBlockEvent) === annotation
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断事务列表中是否包含任一注解。
|
||||
*/
|
||||
export function transactionsHasAnnotationsAny(
|
||||
transactions: readonly Transaction[],
|
||||
annotations: readonly string[]
|
||||
) {
|
||||
return transactions.some(tr => {
|
||||
const value = tr.annotation(codeBlockEvent);
|
||||
return value ? annotations.includes(value) : false;
|
||||
});
|
||||
}
|
||||
@@ -2,11 +2,12 @@
|
||||
* Block 命令
|
||||
*/
|
||||
|
||||
import { EditorSelection } from "@codemirror/state";
|
||||
import { EditorSelection, Transaction } from "@codemirror/state";
|
||||
import { Command } from "@codemirror/view";
|
||||
import { blockState, getActiveNoteBlock, getFirstNoteBlock, getLastNoteBlock, getNoteBlockFromPos } from "./state";
|
||||
import { Block, EditorOptions, DELIMITER_REGEX } from "./types";
|
||||
import { formatBlockContent } from "./formatCode";
|
||||
import { codeBlockEvent, LANGUAGE_CHANGE, ADD_NEW_BLOCK, MOVE_BLOCK, DELETE_BLOCK, CURRENCIES_LOADED, USER_EVENTS } from "./annotation";
|
||||
|
||||
/**
|
||||
* 获取块分隔符
|
||||
@@ -32,7 +33,7 @@ export const insertNewBlockAtCursor = (options: EditorOptions): Command => ({ st
|
||||
|
||||
dispatch(state.replaceSelection(delimText), {
|
||||
scrollIntoView: true,
|
||||
userEvent: "input",
|
||||
userEvent: USER_EVENTS.INPUT,
|
||||
});
|
||||
|
||||
return true;
|
||||
@@ -49,15 +50,16 @@ export const addNewBlockBeforeCurrent = (options: EditorOptions): Command => ({
|
||||
|
||||
const delimText = getBlockDelimiter(options.defaultBlockToken, options.defaultBlockAutoDetect);
|
||||
|
||||
dispatch(state.update({
|
||||
dispatch(state.update({
|
||||
changes: {
|
||||
from: block.delimiter.from,
|
||||
insert: delimText,
|
||||
},
|
||||
selection: EditorSelection.cursor(block.delimiter.from + delimText.length),
|
||||
annotations: [codeBlockEvent.of(ADD_NEW_BLOCK)],
|
||||
}, {
|
||||
scrollIntoView: true,
|
||||
userEvent: "input",
|
||||
userEvent: USER_EVENTS.INPUT,
|
||||
}));
|
||||
|
||||
return true;
|
||||
@@ -74,15 +76,16 @@ export const addNewBlockAfterCurrent = (options: EditorOptions): Command => ({ s
|
||||
|
||||
const delimText = getBlockDelimiter(options.defaultBlockToken, options.defaultBlockAutoDetect);
|
||||
|
||||
dispatch(state.update({
|
||||
dispatch(state.update({
|
||||
changes: {
|
||||
from: block.content.to,
|
||||
insert: delimText,
|
||||
},
|
||||
selection: EditorSelection.cursor(block.content.to + delimText.length)
|
||||
selection: EditorSelection.cursor(block.content.to + delimText.length),
|
||||
annotations: [codeBlockEvent.of(ADD_NEW_BLOCK)],
|
||||
}, {
|
||||
scrollIntoView: true,
|
||||
userEvent: "input",
|
||||
userEvent: USER_EVENTS.INPUT,
|
||||
}));
|
||||
|
||||
return true;
|
||||
@@ -99,15 +102,16 @@ export const addNewBlockBeforeFirst = (options: EditorOptions): Command => ({ st
|
||||
|
||||
const delimText = getBlockDelimiter(options.defaultBlockToken, options.defaultBlockAutoDetect);
|
||||
|
||||
dispatch(state.update({
|
||||
dispatch(state.update({
|
||||
changes: {
|
||||
from: block.delimiter.from,
|
||||
insert: delimText,
|
||||
},
|
||||
selection: EditorSelection.cursor(delimText.length),
|
||||
annotations: [codeBlockEvent.of(ADD_NEW_BLOCK)],
|
||||
}, {
|
||||
scrollIntoView: true,
|
||||
userEvent: "input",
|
||||
userEvent: USER_EVENTS.INPUT,
|
||||
}));
|
||||
|
||||
return true;
|
||||
@@ -124,15 +128,16 @@ export const addNewBlockAfterLast = (options: EditorOptions): Command => ({ stat
|
||||
|
||||
const delimText = getBlockDelimiter(options.defaultBlockToken, options.defaultBlockAutoDetect);
|
||||
|
||||
dispatch(state.update({
|
||||
dispatch(state.update({
|
||||
changes: {
|
||||
from: block.content.to,
|
||||
insert: delimText,
|
||||
},
|
||||
selection: EditorSelection.cursor(block.content.to + delimText.length)
|
||||
selection: EditorSelection.cursor(block.content.to + delimText.length),
|
||||
annotations: [codeBlockEvent.of(ADD_NEW_BLOCK)],
|
||||
}, {
|
||||
scrollIntoView: true,
|
||||
userEvent: "input",
|
||||
userEvent: USER_EVENTS.INPUT,
|
||||
}));
|
||||
|
||||
return true;
|
||||
@@ -143,26 +148,19 @@ export const addNewBlockAfterLast = (options: EditorOptions): Command => ({ stat
|
||||
*/
|
||||
export function changeLanguageTo(state: any, dispatch: any, block: Block, language: string, auto: boolean) {
|
||||
if (state.readOnly) return false;
|
||||
|
||||
const currentDelimiter = state.doc.sliceString(block.delimiter.from, block.delimiter.to);
|
||||
|
||||
// 重置正则表达式的 lastIndex
|
||||
DELIMITER_REGEX.lastIndex = 0;
|
||||
if (currentDelimiter.match(DELIMITER_REGEX)) {
|
||||
const newDelimiter = `\n∞∞∞${language}${auto ? '-a' : ''}\n`;
|
||||
|
||||
dispatch({
|
||||
changes: {
|
||||
from: block.delimiter.from,
|
||||
to: block.delimiter.to,
|
||||
insert: newDelimiter,
|
||||
},
|
||||
});
|
||||
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
const newDelimiter = `\n∞∞∞${language}${auto ? '-a' : ''}\n`;
|
||||
|
||||
dispatch({
|
||||
changes: {
|
||||
from: block.delimiter.from,
|
||||
to: block.delimiter.to,
|
||||
insert: newDelimiter,
|
||||
},
|
||||
annotations: [codeBlockEvent.of(LANGUAGE_CHANGE)],
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -189,7 +187,7 @@ function updateSel(sel: EditorSelection, by: (range: any) => any): EditorSelecti
|
||||
}
|
||||
|
||||
function setSel(state: any, selection: EditorSelection) {
|
||||
return state.update({ selection, scrollIntoView: true, userEvent: "select" });
|
||||
return state.update({ selection, scrollIntoView: true, userEvent: USER_EVENTS.SELECT });
|
||||
}
|
||||
|
||||
function extendSel(state: any, dispatch: any, how: (range: any) => any) {
|
||||
@@ -303,10 +301,11 @@ export const deleteBlock = (_options: EditorOptions): Command => ({ state, dispa
|
||||
to: block.range.to,
|
||||
insert: ""
|
||||
},
|
||||
selection: EditorSelection.cursor(newCursorPos)
|
||||
selection: EditorSelection.cursor(newCursorPos),
|
||||
annotations: [codeBlockEvent.of(DELETE_BLOCK)]
|
||||
}, {
|
||||
scrollIntoView: true,
|
||||
userEvent: "delete"
|
||||
userEvent: USER_EVENTS.DELETE
|
||||
}));
|
||||
|
||||
return true;
|
||||
@@ -366,10 +365,11 @@ function moveCurrentBlock(state: any, dispatch: any, up: boolean) {
|
||||
|
||||
dispatch(state.update({
|
||||
changes,
|
||||
selection: EditorSelection.cursor(newCursorPos)
|
||||
selection: EditorSelection.cursor(newCursorPos),
|
||||
annotations: [codeBlockEvent.of(MOVE_BLOCK)]
|
||||
}, {
|
||||
scrollIntoView: true,
|
||||
userEvent: "move"
|
||||
userEvent: USER_EVENTS.MOVE
|
||||
}));
|
||||
|
||||
return true;
|
||||
@@ -380,4 +380,21 @@ function moveCurrentBlock(state: any, dispatch: any, up: boolean) {
|
||||
*/
|
||||
export const formatCurrentBlock: Command = (view) => {
|
||||
return formatBlockContent(view);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 触发一次货币数据刷新,让数学块重新计算
|
||||
*/
|
||||
export function triggerCurrenciesLoaded({ state, dispatch }: { state: any; dispatch: any }) {
|
||||
if (!dispatch || state.readOnly) {
|
||||
return false;
|
||||
}
|
||||
dispatch(state.update({
|
||||
changes: { from: 0, to: 0, insert: "" },
|
||||
annotations: [
|
||||
codeBlockEvent.of(CURRENCIES_LOADED),
|
||||
Transaction.addToHistory.of(false)
|
||||
],
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { EditorState, EditorSelection } from "@codemirror/state";
|
||||
import { EditorView } from "@codemirror/view";
|
||||
import { Command } from "@codemirror/view";
|
||||
import { LANGUAGES } from "./lang-parser/languages";
|
||||
import { USER_EVENTS } from "./annotation";
|
||||
|
||||
/**
|
||||
* 构建块分隔符正则表达式
|
||||
@@ -89,7 +90,7 @@ export const codeBlockCopyCut = EditorView.domEventHandlers({
|
||||
view.dispatch({
|
||||
changes: ranges,
|
||||
scrollIntoView: true,
|
||||
userEvent: "delete.cut"
|
||||
userEvent: USER_EVENTS.DELETE_CUT
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -111,7 +112,7 @@ const copyCut = (view: EditorView, cut: boolean): boolean => {
|
||||
view.dispatch({
|
||||
changes: ranges,
|
||||
scrollIntoView: true,
|
||||
userEvent: "delete.cut"
|
||||
userEvent: USER_EVENTS.DELETE_CUT
|
||||
});
|
||||
}
|
||||
|
||||
@@ -142,7 +143,7 @@ function doPaste(view: EditorView, input: string) {
|
||||
}
|
||||
|
||||
view.dispatch(changes, {
|
||||
userEvent: "input.paste",
|
||||
userEvent: USER_EVENTS.INPUT_PASTE,
|
||||
scrollIntoView: true
|
||||
});
|
||||
}
|
||||
@@ -186,4 +187,4 @@ export function getCopyPasteExtensions() {
|
||||
return [
|
||||
codeBlockCopyCut,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { EditorView } from '@codemirror/view';
|
||||
import { EditorSelection } from '@codemirror/state';
|
||||
import { blockState } from './state';
|
||||
import { Block } from './types';
|
||||
import { USER_EVENTS } from './annotation';
|
||||
|
||||
/**
|
||||
* 二分查找:找到包含指定位置的块
|
||||
@@ -136,7 +137,7 @@ export function createCursorProtection() {
|
||||
view.dispatch({
|
||||
selection: EditorSelection.cursor(adjustedPos),
|
||||
scrollIntoView: true,
|
||||
userEvent: 'select'
|
||||
userEvent: USER_EVENTS.SELECT
|
||||
});
|
||||
|
||||
// 阻止默认行为
|
||||
@@ -148,4 +149,3 @@ export function createCursorProtection() {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import { ViewPlugin, EditorView, Decoration, WidgetType, layer, RectangleMarker } from "@codemirror/view";
|
||||
import { StateField, RangeSetBuilder, EditorState } from "@codemirror/state";
|
||||
import { blockState } from "./state";
|
||||
import { codeBlockEvent } from "./annotation";
|
||||
|
||||
/**
|
||||
* 块开始装饰组件
|
||||
@@ -180,10 +181,11 @@ const blockLayer = layer({
|
||||
*/
|
||||
const preventFirstBlockFromBeingDeleted = EditorState.changeFilter.of((tr: any) => {
|
||||
const protect: number[] = [];
|
||||
const internalEvent = tr.annotation(codeBlockEvent);
|
||||
|
||||
// 获取块状态并获取第一个块的分隔符大小
|
||||
const blocks = tr.startState.field(blockState);
|
||||
if (blocks && blocks.length > 0) {
|
||||
if (!internalEvent && blocks && blocks.length > 0) {
|
||||
const firstBlock = blocks[0];
|
||||
const firstBlockDelimiterSize = firstBlock.delimiter.to;
|
||||
|
||||
@@ -195,22 +197,25 @@ const preventFirstBlockFromBeingDeleted = EditorState.changeFilter.of((tr: any)
|
||||
|
||||
// 如果是搜索替换操作,保护所有块分隔符
|
||||
if (tr.annotations.some((a: any) => a.value === "input.replace" || a.value === "input.replace.all")) {
|
||||
blocks.forEach((block: any) => {
|
||||
blocks?.forEach((block: any) => {
|
||||
if (block.delimiter) {
|
||||
protect.push(block.delimiter.from, block.delimiter.to);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 返回保护范围数组,如果没有需要保护的范围则返回 false
|
||||
return protect.length > 0 ? protect : false;
|
||||
});
|
||||
// 返回保护范围数组;若无需保护则返回 true 放行事务
|
||||
return protect.length > 0 ? protect : true;
|
||||
})
|
||||
|
||||
/**
|
||||
* 防止选择在第一个块之前
|
||||
* 使用 transactionFilter 来确保选择不会在第一个块之前
|
||||
*/
|
||||
const preventSelectionBeforeFirstBlock = EditorState.transactionFilter.of((tr: any) => {
|
||||
if (tr.annotation(codeBlockEvent)) {
|
||||
return tr;
|
||||
}
|
||||
// 获取块状态并获取第一个块的分隔符大小
|
||||
const blocks = tr.startState.field(blockState);
|
||||
if (!blocks || blocks.length === 0) {
|
||||
@@ -261,4 +266,4 @@ export function getBlockDecorationExtensions(options: {
|
||||
}
|
||||
|
||||
return extensions;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { EditorSelection, SelectionRange } from "@codemirror/state";
|
||||
import { EditorView } from "@codemirror/view";
|
||||
import { getNoteBlockFromPos } from "./state";
|
||||
import { USER_EVENTS } from "./annotation";
|
||||
|
||||
interface LineBlock {
|
||||
from: number;
|
||||
@@ -87,7 +88,7 @@ export const deleteLine = (view: EditorView): boolean => {
|
||||
changes,
|
||||
selection,
|
||||
scrollIntoView: true,
|
||||
userEvent: "delete.line"
|
||||
userEvent: USER_EVENTS.DELETE_LINE
|
||||
});
|
||||
|
||||
return true;
|
||||
@@ -127,8 +128,8 @@ export const deleteLineCommand = ({ state, dispatch }: { state: any; dispatch: a
|
||||
changes,
|
||||
selection,
|
||||
scrollIntoView: true,
|
||||
userEvent: "delete.line"
|
||||
userEvent: USER_EVENTS.DELETE_LINE
|
||||
}));
|
||||
|
||||
return true;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ import * as prettier from "prettier/standalone";
|
||||
import { getActiveNoteBlock } from "./state";
|
||||
import { getLanguage } from "./lang-parser/languages";
|
||||
import { SupportedLanguage } from "./types";
|
||||
import { USER_EVENTS } from "./annotation";
|
||||
|
||||
export const formatBlockContent = (view) => {
|
||||
if (!view || view.state.readOnly)
|
||||
@@ -87,7 +88,7 @@ export const formatBlockContent = (view) => {
|
||||
},
|
||||
selection: EditorSelection.cursor(currentBlockFrom + Math.min(formattedContent.cursorOffset, formattedContent.formatted.length)),
|
||||
scrollIntoView: true,
|
||||
userEvent: "input"
|
||||
userEvent: USER_EVENTS.INPUT
|
||||
});
|
||||
|
||||
return true;
|
||||
@@ -100,4 +101,4 @@ export const formatBlockContent = (view) => {
|
||||
// 执行异步格式化
|
||||
performFormat();
|
||||
return true; // 立即返回 true,表示命令已开始执行
|
||||
};
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { ViewPlugin, Decoration, WidgetType } from "@codemirror/view";
|
||||
import { RangeSetBuilder } from "@codemirror/state";
|
||||
import { getNoteBlockFromPos } from "./state";
|
||||
import { transactionsHasAnnotation, CURRENCIES_LOADED } from "./annotation";
|
||||
// 声明全局math对象
|
||||
declare global {
|
||||
interface Window {
|
||||
@@ -75,6 +76,11 @@ function mathDeco(view: any): any {
|
||||
// get math.js parser and cache it for this block
|
||||
let { parser, prev } = mathParsers.get(block) || {};
|
||||
if (!parser) {
|
||||
// 如果当前可见行不是该 math 块的第一行,为了正确累计 prev,需要从块头开始重新扫描
|
||||
if (line.from > block.content.from) {
|
||||
pos = block.content.from;
|
||||
continue;
|
||||
}
|
||||
if (typeof window.math !== 'undefined') {
|
||||
parser = window.math.parser();
|
||||
mathParsers.set(block, { parser, prev });
|
||||
@@ -148,8 +154,12 @@ export const mathBlock = ViewPlugin.fromClass(class {
|
||||
}
|
||||
|
||||
update(update: any) {
|
||||
// If the document changed, the viewport changed, update the decorations
|
||||
if (update.docChanged || update.viewportChanged) {
|
||||
// 需要在文档/视口变化或收到 CURRENCIES_LOADED 注解时重新渲染
|
||||
if (
|
||||
update.docChanged ||
|
||||
update.viewportChanged ||
|
||||
transactionsHasAnnotation(update.transactions, CURRENCIES_LOADED)
|
||||
) {
|
||||
this.decorations = mathDeco(update.view);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { EditorSelection, SelectionRange } from "@codemirror/state";
|
||||
import { blockState } from "./state";
|
||||
import { LANGUAGES } from "./lang-parser/languages";
|
||||
import { USER_EVENTS } from "./annotation";
|
||||
|
||||
interface LineBlock {
|
||||
from: number;
|
||||
@@ -131,7 +132,7 @@ function moveLine(state: any, dispatch: any, forward: boolean): boolean {
|
||||
changes,
|
||||
scrollIntoView: true,
|
||||
selection: EditorSelection.create(ranges, state.selection.mainIndex),
|
||||
userEvent: "move.line"
|
||||
userEvent: USER_EVENTS.MOVE_LINE
|
||||
}));
|
||||
|
||||
return true;
|
||||
@@ -157,4 +158,4 @@ export const moveLineUp = ({ state, dispatch }: { state: any; dispatch: any }):
|
||||
*/
|
||||
export const moveLineDown = ({ state, dispatch }: { state: any; dispatch: any }): boolean => {
|
||||
return moveLine(state, dispatch, true);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@ import { StateField, StateEffect, RangeSetBuilder, EditorSelection, EditorState,
|
||||
import { selectAll as defaultSelectAll } from "@codemirror/commands";
|
||||
import { Command } from "@codemirror/view";
|
||||
import { getActiveNoteBlock, blockState } from "./state";
|
||||
import { USER_EVENTS } from "./annotation";
|
||||
|
||||
/**
|
||||
* 当用户按下 Ctrl+A 时,我们希望首先选择整个块。但如果整个块已经被选中,
|
||||
@@ -115,7 +116,7 @@ export const selectAll: Command = ({ state, dispatch }) => {
|
||||
// 选择当前块的所有内容
|
||||
dispatch(state.update({
|
||||
selection: { anchor: block.content.from, head: block.content.to },
|
||||
userEvent: "select"
|
||||
userEvent: USER_EVENTS.SELECT
|
||||
}));
|
||||
|
||||
return true;
|
||||
@@ -127,7 +128,7 @@ export const selectAll: Command = ({ state, dispatch }) => {
|
||||
*/
|
||||
export const blockAwareSelection = EditorState.transactionFilter.of((tr: any) => {
|
||||
// 只处理选择变化的事务,并且忽略我们自己生成的事务
|
||||
if (!tr.selection || !tr.selection.ranges || tr.annotation?.(Transaction.userEvent) === "select.block-boundary") {
|
||||
if (!tr.selection || !tr.selection.ranges || tr.annotation?.(Transaction.userEvent) === USER_EVENTS.SELECT_BLOCK_BOUNDARY) {
|
||||
return tr;
|
||||
}
|
||||
|
||||
@@ -181,7 +182,7 @@ export const blockAwareSelection = EditorState.transactionFilter.of((tr: any) =>
|
||||
return {
|
||||
...tr,
|
||||
selection: EditorSelection.create(correctedRanges, tr.selection.mainIndex),
|
||||
annotations: tr.annotations.concat(Transaction.userEvent.of("select.block-boundary"))
|
||||
annotations: tr.annotations.concat(Transaction.userEvent.of(USER_EVENTS.SELECT_BLOCK_BOUNDARY))
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -219,4 +220,4 @@ export function getBlockSelectExtensions() {
|
||||
return [
|
||||
emptyBlockSelected,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import { EditorSelection, findClusterBreak } from "@codemirror/state";
|
||||
import { getNoteBlockFromPos } from "./state";
|
||||
import { USER_EVENTS } from "./annotation";
|
||||
|
||||
/**
|
||||
* 交换光标前后的字符
|
||||
@@ -46,8 +47,8 @@ export const transposeChars = ({ state, dispatch }: { state: any; dispatch: any
|
||||
|
||||
dispatch(state.update(changes, {
|
||||
scrollIntoView: true,
|
||||
userEvent: "move.character"
|
||||
userEvent: USER_EVENTS.MOVE_CHARACTER
|
||||
}));
|
||||
|
||||
return true;
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user