mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
Copy and paste appflowy editor data (#2714)
* feat: copy and paste appflowy editor data * fix: review suggestion
This commit is contained in:
parent
f86a98cd51
commit
d02b8c609b
@ -8,6 +8,8 @@ import isHotkey from 'is-hotkey';
|
||||
import { deleteRangeAndInsertEnterThunk } from '$app_reducers/document/async-actions/range';
|
||||
import { useRangeRef } from '$app/components/document/_shared/SubscribeSelection.hooks';
|
||||
import { isPrintableKeyEvent } from '$app/utils/document/action';
|
||||
import { toggleFormatThunk } from '$app_reducers/document/async-actions/format';
|
||||
import { isFormatHotkey, parseFormat } from '$app/utils/document/format';
|
||||
|
||||
export function useRangeKeyDown() {
|
||||
const rangeRef = useRangeRef();
|
||||
@ -33,7 +35,7 @@ export function useRangeKeyDown() {
|
||||
{
|
||||
// handle char input
|
||||
canHandle: (e: KeyboardEvent) => {
|
||||
return isPrintableKeyEvent(e);
|
||||
return isPrintableKeyEvent(e) && !e.shiftKey && !e.ctrlKey && !e.metaKey;
|
||||
},
|
||||
handler: (e: KeyboardEvent) => {
|
||||
if (!controller) return;
|
||||
@ -94,11 +96,26 @@ export function useRangeKeyDown() {
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
// handle format shortcuts
|
||||
canHandle: isFormatHotkey,
|
||||
handler: (e: KeyboardEvent) => {
|
||||
if (!controller) return;
|
||||
const format = parseFormat(e);
|
||||
if (!format) return;
|
||||
dispatch(
|
||||
toggleFormatThunk({
|
||||
format,
|
||||
controller,
|
||||
})
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
[controller, dispatch]
|
||||
);
|
||||
|
||||
const onKeyDown = useCallback(
|
||||
const onKeyDownCapture = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
if (!rangeRef.current) {
|
||||
return;
|
||||
@ -108,12 +125,16 @@ export function useRangeKeyDown() {
|
||||
return;
|
||||
}
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
const filteredEvents = interceptEvents.filter((event) => event.canHandle(e));
|
||||
filteredEvents.forEach((event) => event.handler(e));
|
||||
const lastIndex = filteredEvents.length - 1;
|
||||
if (lastIndex < 0) {
|
||||
return;
|
||||
}
|
||||
const lastEvent = filteredEvents[lastIndex];
|
||||
lastEvent?.handler(e);
|
||||
},
|
||||
[interceptEvents, rangeRef]
|
||||
);
|
||||
|
||||
return onKeyDown;
|
||||
return onKeyDownCapture;
|
||||
}
|
||||
|
@ -14,8 +14,8 @@ export function useKeyDown(id: string) {
|
||||
const customEvents = useMemo(() => {
|
||||
return [
|
||||
...commonKeyEvents,
|
||||
|
||||
{
|
||||
// rewrite only shift + enter key and no other key is pressed
|
||||
canHandle: (e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
return isHotkey(Keyboard.keys.SHIFT_ENTER, e);
|
||||
},
|
||||
|
@ -3,8 +3,12 @@ import BlockSideToolbar from '../BlockSideToolbar';
|
||||
import BlockSelection from '../BlockSelection';
|
||||
import TextActionMenu from '$app/components/document/TextActionMenu';
|
||||
import BlockSlash from '$app/components/document/BlockSlash';
|
||||
import { useCopy } from '$app/components/document/_shared/CopyPasteHooks/useCopy';
|
||||
import { usePaste } from '$app/components/document/_shared/CopyPasteHooks/usePaste';
|
||||
|
||||
export default function Overlay({ container }: { container: HTMLDivElement }) {
|
||||
useCopy(container);
|
||||
usePaste(container);
|
||||
return (
|
||||
<>
|
||||
<BlockSideToolbar container={container} />
|
||||
|
@ -20,7 +20,7 @@ export function useKeyDown(id: string) {
|
||||
return [
|
||||
...commonKeyEvents,
|
||||
{
|
||||
// Prevent all enter key
|
||||
// Prevent all enter key unless it be rewritten
|
||||
canHandle: (e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
return e.key === Keyboard.keys.ENTER;
|
||||
},
|
||||
@ -29,7 +29,7 @@ export function useKeyDown(id: string) {
|
||||
},
|
||||
},
|
||||
{
|
||||
// handle enter key and no other key is pressed
|
||||
// rewrite only enter key and no other key is pressed
|
||||
canHandle: (e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
return isHotkey(Keyboard.keys.ENTER, e);
|
||||
},
|
||||
@ -43,9 +43,8 @@ export function useKeyDown(id: string) {
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
// Prevent tab key from indenting
|
||||
// Prevent all tab key unless it be rewritten
|
||||
canHandle: (e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
return e.key === Keyboard.keys.TAB;
|
||||
},
|
||||
@ -54,7 +53,7 @@ export function useKeyDown(id: string) {
|
||||
},
|
||||
},
|
||||
{
|
||||
// handle tab key and no other key is pressed
|
||||
// rewrite only tab key and no other key is pressed
|
||||
canHandle: (e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
return isHotkey(Keyboard.keys.TAB, e);
|
||||
},
|
||||
@ -69,7 +68,7 @@ export function useKeyDown(id: string) {
|
||||
},
|
||||
},
|
||||
{
|
||||
// handle shift + tab key and no other key is pressed
|
||||
// rewrite only shift+tab key and no other key is pressed
|
||||
canHandle: (e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
return isHotkey(Keyboard.keys.SHIFT_TAB, e);
|
||||
},
|
||||
@ -83,14 +82,12 @@ export function useKeyDown(id: string) {
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
...turnIntoEvents,
|
||||
];
|
||||
}, [commonKeyEvents, controller, dispatch, id, turnIntoEvents]);
|
||||
|
||||
const onKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
|
||||
e.stopPropagation();
|
||||
|
||||
const filteredEvents = interceptEvents.filter((event) => event.canHandle(e));
|
||||
|
@ -0,0 +1,37 @@
|
||||
import { useCallback, useContext, useEffect } from 'react';
|
||||
import { copyThunk } from '$app_reducers/document/async-actions/copyPaste';
|
||||
import { useAppDispatch } from '$app/stores/store';
|
||||
import { DocumentControllerContext } from '$app/stores/effects/document/document_controller';
|
||||
import { BlockCopyData } from '$app/interfaces/document';
|
||||
import { clipboardTypes } from '$app/constants/document/copy_paste';
|
||||
|
||||
export function useCopy(container: HTMLDivElement) {
|
||||
const dispatch = useAppDispatch();
|
||||
const controller = useContext(DocumentControllerContext);
|
||||
|
||||
const handleCopyCapture = useCallback(
|
||||
(e: ClipboardEvent) => {
|
||||
if (!controller) return;
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
const setClipboardData = (data: BlockCopyData) => {
|
||||
e.clipboardData?.setData(clipboardTypes.JSON, data.json);
|
||||
e.clipboardData?.setData(clipboardTypes.TEXT, data.text);
|
||||
e.clipboardData?.setData(clipboardTypes.HTML, data.html);
|
||||
};
|
||||
dispatch(
|
||||
copyThunk({
|
||||
setClipboardData,
|
||||
})
|
||||
);
|
||||
},
|
||||
[controller, dispatch]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
container.addEventListener('copy', handleCopyCapture, true);
|
||||
return () => {
|
||||
container.removeEventListener('copy', handleCopyCapture, true);
|
||||
};
|
||||
}, [container, handleCopyCapture]);
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
import { useCallback, useContext, useEffect } from 'react';
|
||||
import { useAppDispatch } from '$app/stores/store';
|
||||
import { DocumentControllerContext } from '$app/stores/effects/document/document_controller';
|
||||
import { pasteThunk } from '$app_reducers/document/async-actions/copyPaste';
|
||||
import { clipboardTypes } from '$app/constants/document/copy_paste';
|
||||
|
||||
export function usePaste(container: HTMLDivElement) {
|
||||
const dispatch = useAppDispatch();
|
||||
const controller = useContext(DocumentControllerContext);
|
||||
|
||||
const handlePasteCapture = useCallback(
|
||||
(e: ClipboardEvent) => {
|
||||
if (!controller) return;
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
dispatch(
|
||||
pasteThunk({
|
||||
controller,
|
||||
data: {
|
||||
json: e.clipboardData?.getData(clipboardTypes.JSON) || '',
|
||||
text: e.clipboardData?.getData(clipboardTypes.TEXT) || '',
|
||||
html: e.clipboardData?.getData(clipboardTypes.HTML) || '',
|
||||
},
|
||||
})
|
||||
);
|
||||
},
|
||||
[controller, dispatch]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
container.addEventListener('paste', handlePasteCapture, true);
|
||||
return () => {
|
||||
container.removeEventListener('paste', handlePasteCapture, true);
|
||||
};
|
||||
}, [container, handlePasteCapture]);
|
||||
}
|
@ -10,6 +10,8 @@ import { useContext, useMemo } from 'react';
|
||||
import { useFocused } from '$app/components/document/_shared/SubscribeSelection.hooks';
|
||||
import { DocumentControllerContext } from '$app/stores/effects/document/document_controller';
|
||||
import { useAppDispatch } from '$app/stores/store';
|
||||
import { isFormatHotkey, parseFormat } from '$app/utils/document/format';
|
||||
import { toggleFormatThunk } from '$app_reducers/document/async-actions/format';
|
||||
|
||||
export function useCommonKeyEvents(id: string) {
|
||||
const { focused, caretRef } = useFocused(id);
|
||||
@ -73,6 +75,21 @@ export function useCommonKeyEvents(id: string) {
|
||||
dispatch(rightActionForBlockThunk({ id }));
|
||||
},
|
||||
},
|
||||
{
|
||||
// handle format shortcuts
|
||||
canHandle: isFormatHotkey,
|
||||
handler: (e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (!controller) return;
|
||||
const format = parseFormat(e);
|
||||
if (!format) return;
|
||||
dispatch(
|
||||
toggleFormatThunk({
|
||||
format,
|
||||
controller,
|
||||
})
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
}, [caretRef, controller, dispatch, focused, id]);
|
||||
return commonKeyEvents;
|
||||
|
@ -1,19 +1,19 @@
|
||||
import { EditorProps } from "$app/interfaces/document";
|
||||
import { useCallback, useEffect, useMemo, useRef } from "react";
|
||||
import { ReactEditor } from "slate-react";
|
||||
import { BaseRange, Descendant, Editor, NodeEntry, Range, Selection } from "slate";
|
||||
import { EditorProps } from '$app/interfaces/document';
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { ReactEditor } from 'slate-react';
|
||||
import { BaseRange, Descendant, Editor, NodeEntry, Range, Selection, Transforms } from 'slate';
|
||||
import {
|
||||
converToIndexLength,
|
||||
convertToDelta,
|
||||
convertToSlateSelection,
|
||||
indent,
|
||||
outdent
|
||||
} from "$app/utils/document/slate_editor";
|
||||
import { focusNodeByIndex } from "$app/utils/document/node";
|
||||
import { Keyboard } from "$app/constants/document/keyboard";
|
||||
import Delta from "quill-delta";
|
||||
import isHotkey from "is-hotkey";
|
||||
import { useSlateYjs } from "$app/components/document/_shared/SlateEditor/useSlateYjs";
|
||||
outdent,
|
||||
} from '$app/utils/document/slate_editor';
|
||||
import { focusNodeByIndex } from '$app/utils/document/node';
|
||||
import { Keyboard } from '$app/constants/document/keyboard';
|
||||
import Delta from 'quill-delta';
|
||||
import isHotkey from 'is-hotkey';
|
||||
import { useSlateYjs } from '$app/components/document/_shared/SlateEditor/useSlateYjs';
|
||||
|
||||
export function useEditor({
|
||||
onChange,
|
||||
@ -109,7 +109,6 @@ export function useEditor({
|
||||
[editor, onKeyDown, isCodeBlock]
|
||||
);
|
||||
|
||||
|
||||
const onBlur = useCallback(
|
||||
(_event: React.FocusEvent<HTMLDivElement>) => {
|
||||
editor.deselect();
|
||||
@ -122,10 +121,9 @@ export function useEditor({
|
||||
const slateSelection = convertToSlateSelection(selection.index, selection.length, editor.children);
|
||||
if (!slateSelection) return;
|
||||
const isFocused = ReactEditor.isFocused(editor);
|
||||
|
||||
if (isFocused && JSON.stringify(slateSelection) === JSON.stringify(editor.selection)) return;
|
||||
|
||||
focusNodeByIndex(ref.current, selection.index, selection.length);
|
||||
Transforms.select(editor, slateSelection);
|
||||
}, [editor, selection]);
|
||||
|
||||
return {
|
||||
@ -139,4 +137,3 @@ export function useEditor({
|
||||
onBlur,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -1,16 +1,16 @@
|
||||
import Delta from "quill-delta";
|
||||
import { useEffect, useMemo, useRef } from "react";
|
||||
import * as Y from "yjs";
|
||||
import { convertToSlateValue } from "$app/utils/document/slate_editor";
|
||||
import { slateNodesToInsertDelta, withYjs, YjsEditor } from "@slate-yjs/core";
|
||||
import { withReact } from "slate-react";
|
||||
import { createEditor } from "slate";
|
||||
import Delta from 'quill-delta';
|
||||
import { useEffect, useMemo, useRef } from 'react';
|
||||
import * as Y from 'yjs';
|
||||
import { convertToSlateValue } from '$app/utils/document/slate_editor';
|
||||
import { slateNodesToInsertDelta, withYjs, YjsEditor } from '@slate-yjs/core';
|
||||
import { withReact } from 'slate-react';
|
||||
import { createEditor } from 'slate';
|
||||
|
||||
export function useSlateYjs({ delta }: { delta?: Delta }) {
|
||||
const yTextRef = useRef<Y.Text>();
|
||||
const sharedType = useMemo(() => {
|
||||
const yDoc = new Y.Doc();
|
||||
const sharedType = yDoc.get("content", Y.XmlText) as Y.XmlText;
|
||||
const sharedType = yDoc.get('content', Y.XmlText) as Y.XmlText;
|
||||
const value = convertToSlateValue(delta || new Delta());
|
||||
const insertDelta = slateNodesToInsertDelta(value);
|
||||
sharedType.applyDelta(insertDelta);
|
||||
@ -40,4 +40,4 @@ export function useSlateYjs({ delta }: { delta?: Delta }) {
|
||||
}, [delta, editor]);
|
||||
|
||||
return editor;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,5 @@
|
||||
export const clipboardTypes = {
|
||||
JSON: 'application/json',
|
||||
TEXT: 'text/plain',
|
||||
HTML: 'text/html',
|
||||
};
|
@ -28,5 +28,15 @@ export const Keyboard = {
|
||||
Space: ' ',
|
||||
Reduce: '-',
|
||||
BackQuote: '`',
|
||||
FORMAT: {
|
||||
BOLD: 'Mod+b',
|
||||
ITALIC: 'Mod+i',
|
||||
UNDERLINE: 'Mod+u',
|
||||
STRIKE: 'Mod+Shift+s',
|
||||
CODE: 'Mod+Shift+c',
|
||||
},
|
||||
COPY: 'Mod+c',
|
||||
CUT: 'Mod+x',
|
||||
PASTE: 'Mod+v',
|
||||
},
|
||||
};
|
||||
|
@ -3,6 +3,12 @@ import { BlockActionTypePB } from '@/services/backend';
|
||||
import { Sources } from 'quill';
|
||||
import React from 'react';
|
||||
|
||||
export interface DocumentBlockJSON {
|
||||
type: BlockType;
|
||||
data: BlockData<any>;
|
||||
children: DocumentBlockJSON[];
|
||||
}
|
||||
|
||||
export interface RangeStatic {
|
||||
id: string;
|
||||
length: number;
|
||||
@ -12,7 +18,7 @@ export interface RangeStatic {
|
||||
export enum BlockType {
|
||||
PageBlock = 'page',
|
||||
HeadingBlock = 'heading',
|
||||
TextBlock = 'text',
|
||||
TextBlock = 'paragraph',
|
||||
TodoListBlock = 'todo_list',
|
||||
BulletedListBlock = 'bulleted_list',
|
||||
NumberedListBlock = 'numbered_list',
|
||||
@ -252,3 +258,9 @@ export interface EditorProps {
|
||||
onChange?: (delta: Delta, oldDelta: Delta, source?: Sources) => void;
|
||||
onKeyDown?: (event: React.KeyboardEvent<HTMLDivElement>) => void;
|
||||
}
|
||||
|
||||
export interface BlockCopyData {
|
||||
json: string;
|
||||
text: string;
|
||||
html: string;
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { DocumentData, Node } from '@/appflowy_app/interfaces/document';
|
||||
import { DocumentBlockJSON, DocumentData, Node } from '@/appflowy_app/interfaces/document';
|
||||
import { createContext } from 'react';
|
||||
import { DocumentBackendService } from './document_bd_svc';
|
||||
import {
|
||||
|
@ -3,6 +3,7 @@ import { DocumentController } from '$app/stores/effects/document/document_contro
|
||||
import { DocumentState } from '$app/interfaces/document';
|
||||
import Delta from 'quill-delta';
|
||||
import { blockConfig } from '$app/constants/document/config';
|
||||
import { getMoveChildrenActions } from '$app/utils/document/action';
|
||||
|
||||
/**
|
||||
* Merge two blocks
|
||||
@ -33,12 +34,12 @@ export const mergeDeltaThunk = createAsyncThunk(
|
||||
|
||||
const actions = [updateAction];
|
||||
// move children
|
||||
const config = blockConfig[target.type];
|
||||
const children = state.children[source.children].map((id) => state.nodes[id]);
|
||||
const targetParentId = config.canAddChild ? target.id : target.parent;
|
||||
if (!targetParentId) return;
|
||||
const targetPrevId = targetParentId === target.id ? '' : target.id;
|
||||
const moveActions = controller.getMoveChildrenAction(children, targetParentId, targetPrevId);
|
||||
const moveActions = getMoveChildrenActions({
|
||||
controller,
|
||||
children,
|
||||
target,
|
||||
});
|
||||
actions.push(...moveActions);
|
||||
// delete current block
|
||||
const deleteAction = controller.getDeleteAction(source);
|
||||
|
@ -0,0 +1,202 @@
|
||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import { RootState } from '$app/stores/store';
|
||||
import { getMiddleIds, getMoveChildrenActions, getStartAndEndIdsByRange } from '$app/utils/document/action';
|
||||
import { BlockCopyData, BlockType, DocumentBlockJSON } from '$app/interfaces/document';
|
||||
import Delta from 'quill-delta';
|
||||
import { getDeltaByRange } from '$app/utils/document/delta';
|
||||
import { deleteRangeAndInsertThunk } from '$app_reducers/document/async-actions/range';
|
||||
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
||||
import {
|
||||
generateBlocks,
|
||||
getAppendBlockDeltaAction,
|
||||
getCopyBlock,
|
||||
getInsertBlockActions,
|
||||
} from '$app/utils/document/copy_paste';
|
||||
import { rangeActions } from '$app_reducers/document/slice';
|
||||
|
||||
export const copyThunk = createAsyncThunk<
|
||||
void,
|
||||
{
|
||||
setClipboardData: (data: BlockCopyData) => void;
|
||||
}
|
||||
>('document/copy', async (payload, thunkAPI) => {
|
||||
const { getState } = thunkAPI;
|
||||
const { setClipboardData } = payload;
|
||||
const state = getState() as RootState;
|
||||
const { document, documentRange } = state;
|
||||
const startAndEndIds = getStartAndEndIdsByRange(documentRange);
|
||||
if (startAndEndIds.length === 0) return;
|
||||
const result: DocumentBlockJSON[] = [];
|
||||
if (startAndEndIds.length === 1) {
|
||||
// copy single block
|
||||
const id = startAndEndIds[0];
|
||||
const node = document.nodes[id];
|
||||
const nodeDelta = new Delta(node.data.delta);
|
||||
const range = documentRange.ranges[id] || { index: 0, length: 0 };
|
||||
const isFull = range.index === 0 && range.length === nodeDelta.length();
|
||||
if (isFull) {
|
||||
result.push(getCopyBlock(id, document, documentRange));
|
||||
} else {
|
||||
result.push({
|
||||
type: BlockType.TextBlock,
|
||||
children: [],
|
||||
data: {
|
||||
delta: getDeltaByRange(nodeDelta, range).ops,
|
||||
},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// copy multiple blocks
|
||||
const copyIds: string[] = [];
|
||||
const [startId, endId] = startAndEndIds;
|
||||
const middleIds = getMiddleIds(document, startId, endId);
|
||||
copyIds.push(startId, ...middleIds, endId);
|
||||
const map = new Map<string, DocumentBlockJSON>();
|
||||
copyIds.forEach((id) => {
|
||||
const block = getCopyBlock(id, document, documentRange);
|
||||
map.set(id, block);
|
||||
const node = document.nodes[id];
|
||||
const parent = node.parent;
|
||||
if (parent && map.has(parent)) {
|
||||
map.get(parent)!.children.push(block);
|
||||
} else {
|
||||
result.push(block);
|
||||
}
|
||||
});
|
||||
}
|
||||
setClipboardData({
|
||||
json: JSON.stringify(result),
|
||||
// TODO: implement plain text and html
|
||||
text: '',
|
||||
html: '',
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Paste data to document
|
||||
* 1. delete range blocks
|
||||
* 2. if current block is empty text block, insert paste data below current block and delete current block
|
||||
* 3. otherwise:
|
||||
* 3.1 split current block, before part merge the first block of paste data and update current block
|
||||
* 3.2 after part append to the last block of paste data
|
||||
* 3.3 move the first block children of paste data to current block
|
||||
* 3.4 delete the first block of paste data
|
||||
*/
|
||||
export const pasteThunk = createAsyncThunk<
|
||||
void,
|
||||
{
|
||||
data: BlockCopyData;
|
||||
controller: DocumentController;
|
||||
}
|
||||
>('document/paste', async (payload, thunkAPI) => {
|
||||
const { getState, dispatch } = thunkAPI;
|
||||
const { data, controller } = payload;
|
||||
// delete range blocks
|
||||
await dispatch(deleteRangeAndInsertThunk({ controller }));
|
||||
|
||||
let pasteData;
|
||||
if (data.json) {
|
||||
pasteData = JSON.parse(data.json) as DocumentBlockJSON[];
|
||||
} else if (data.text) {
|
||||
// TODO: implement plain text
|
||||
} else if (data.html) {
|
||||
// TODO: implement html
|
||||
}
|
||||
if (!pasteData) return;
|
||||
const { document, documentRange } = getState() as RootState;
|
||||
const { caret } = documentRange;
|
||||
if (!caret) return;
|
||||
const currentBlock = document.nodes[caret.id];
|
||||
if (!currentBlock.parent) return;
|
||||
const pasteBlocks = generateBlocks(pasteData, currentBlock.parent);
|
||||
const currentBlockDelta = new Delta(currentBlock.data.delta);
|
||||
const type = currentBlock.type;
|
||||
const actions = getInsertBlockActions(pasteBlocks, currentBlock.id, controller);
|
||||
const firstPasteBlock = pasteBlocks[0];
|
||||
const firstPasteBlockChildren = pasteBlocks.filter((block) => block.parent === firstPasteBlock.id);
|
||||
|
||||
const lastPasteBlock = pasteBlocks[pasteBlocks.length - 1];
|
||||
if (type === BlockType.TextBlock && currentBlockDelta.length() === 0) {
|
||||
// move current block children to first paste block
|
||||
const children = document.children[currentBlock.children].map((id) => document.nodes[id]);
|
||||
const firstPasteBlockLastChild =
|
||||
firstPasteBlockChildren.length > 0 ? firstPasteBlockChildren[firstPasteBlockChildren.length - 1] : undefined;
|
||||
const prevId = firstPasteBlockLastChild ? firstPasteBlockLastChild.id : undefined;
|
||||
const moveChildrenActions = getMoveChildrenActions({
|
||||
target: firstPasteBlock,
|
||||
children,
|
||||
controller,
|
||||
prevId,
|
||||
});
|
||||
actions.push(...moveChildrenActions);
|
||||
// delete current block
|
||||
actions.push(controller.getDeleteAction(currentBlock));
|
||||
await controller.applyActions(actions);
|
||||
// set caret to the end of the last paste block
|
||||
dispatch(
|
||||
rangeActions.setCaret({
|
||||
id: lastPasteBlock.id,
|
||||
index: new Delta(lastPasteBlock.data.delta).length(),
|
||||
length: 0,
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// split current block
|
||||
const currentBeforeDelta = getDeltaByRange(currentBlockDelta, { index: 0, length: caret.index });
|
||||
const currentAfterDelta = getDeltaByRange(currentBlockDelta, {
|
||||
index: caret.index,
|
||||
length: currentBlockDelta.length() - caret.index,
|
||||
});
|
||||
|
||||
let newCaret;
|
||||
const firstPasteBlockDelta = new Delta(firstPasteBlock.data.delta);
|
||||
const lastPasteBlockDelta = new Delta(lastPasteBlock.data.delta);
|
||||
let mergeDelta = new Delta(currentBeforeDelta.ops).concat(firstPasteBlockDelta);
|
||||
if (firstPasteBlock.id !== lastPasteBlock.id) {
|
||||
// update the last block of paste data
|
||||
actions.push(getAppendBlockDeltaAction(lastPasteBlock, currentAfterDelta, false, controller));
|
||||
newCaret = {
|
||||
id: lastPasteBlock.id,
|
||||
index: lastPasteBlockDelta.length(),
|
||||
length: 0,
|
||||
};
|
||||
} else {
|
||||
newCaret = {
|
||||
id: currentBlock.id,
|
||||
index: mergeDelta.length(),
|
||||
length: 0,
|
||||
};
|
||||
mergeDelta = mergeDelta.concat(currentAfterDelta);
|
||||
}
|
||||
|
||||
// update current block and merge the first block of paste data
|
||||
actions.push(
|
||||
controller.getUpdateAction({
|
||||
...currentBlock,
|
||||
data: {
|
||||
...currentBlock.data,
|
||||
delta: mergeDelta.ops,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
// move the first block children of paste data to current block
|
||||
if (firstPasteBlockChildren.length > 0) {
|
||||
const moveChildrenActions = getMoveChildrenActions({
|
||||
target: currentBlock,
|
||||
children: firstPasteBlockChildren,
|
||||
controller,
|
||||
});
|
||||
actions.push(...moveChildrenActions);
|
||||
}
|
||||
|
||||
// delete first block of paste data
|
||||
actions.push(controller.getDeleteAction(firstPasteBlock));
|
||||
await controller.applyActions(actions);
|
||||
// set caret to the end of the last paste block
|
||||
if (!newCaret) return;
|
||||
|
||||
dispatch(rangeActions.setCaret(newCaret));
|
||||
});
|
@ -3,7 +3,6 @@ import { RootState } from '$app/stores/store';
|
||||
import { TextAction } from '$app/interfaces/document';
|
||||
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
||||
import Delta from 'quill-delta';
|
||||
import { rangeActions } from '$app_reducers/document/slice';
|
||||
|
||||
export const getFormatActiveThunk = createAsyncThunk<boolean, TextAction>(
|
||||
'document/getFormatActive',
|
||||
@ -29,12 +28,17 @@ export const getFormatActiveThunk = createAsyncThunk<boolean, TextAction>(
|
||||
|
||||
export const toggleFormatThunk = createAsyncThunk(
|
||||
'document/toggleFormat',
|
||||
async (payload: { format: TextAction; controller: DocumentController; isActive: boolean }, thunkAPI) => {
|
||||
async (payload: { format: TextAction; controller: DocumentController; isActive?: boolean }, thunkAPI) => {
|
||||
const { getState, dispatch } = thunkAPI;
|
||||
const { format, controller, isActive } = payload;
|
||||
const { format, controller } = payload;
|
||||
let isActive = payload.isActive;
|
||||
if (isActive === undefined) {
|
||||
const { payload: active } = await dispatch(getFormatActiveThunk(format));
|
||||
isActive = !!active;
|
||||
}
|
||||
const state = getState() as RootState;
|
||||
const { document } = state;
|
||||
const { ranges, caret } = state.documentRange;
|
||||
const { ranges } = state.documentRange;
|
||||
|
||||
const toggle = (delta: Delta, format: TextAction) => {
|
||||
const newOps = delta.ops.map((op) => {
|
||||
|
@ -1,15 +1,15 @@
|
||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import { RootState } from '$app/stores/store';
|
||||
import { rangeActions } from '$app_reducers/document/slice';
|
||||
import { getNextLineId } from '$app/utils/document/block';
|
||||
import Delta from 'quill-delta';
|
||||
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
||||
import {
|
||||
getAfterMergeCaretByRange,
|
||||
getInsertEnterNodeAction,
|
||||
getMergeEndDeltaToStartActionsByRange,
|
||||
getMiddleIds,
|
||||
getMiddleIdsByRange,
|
||||
getStartAndEndDeltaExpectRange,
|
||||
getStartAndEndExtentDelta,
|
||||
} from '$app/utils/document/action';
|
||||
import { RangeState, SplitRelationship } from '$app/interfaces/document';
|
||||
import { blockConfig } from '$app/constants/document/config';
|
||||
@ -74,25 +74,19 @@ export const storeRangeThunk = createAsyncThunk('document/storeRange', (payload:
|
||||
const startId = isForward ? anchor.id : focus.id;
|
||||
const endId = isForward ? focus.id : anchor.id;
|
||||
|
||||
let currentId: string | undefined = startId;
|
||||
while (currentId && currentId !== endId) {
|
||||
const nextId = getNextLineId(state.document, currentId);
|
||||
if (nextId && nextId !== endId) {
|
||||
const node = state.document.nodes[nextId];
|
||||
const middleIds = getMiddleIds(state.document, startId, endId);
|
||||
middleIds.forEach((id) => {
|
||||
const node = state.document.nodes[id];
|
||||
|
||||
if (!node || !node.data.delta) return;
|
||||
const delta = new Delta(node.data.delta);
|
||||
if (!node || !node.data.delta) return;
|
||||
const delta = new Delta(node.data.delta);
|
||||
const rangeStatic = {
|
||||
index: 0,
|
||||
length: delta.length(),
|
||||
};
|
||||
|
||||
// set full range
|
||||
const rangeStatic = {
|
||||
index: 0,
|
||||
length: delta.length(),
|
||||
};
|
||||
|
||||
ranges[nextId] = rangeStatic;
|
||||
}
|
||||
currentId = nextId;
|
||||
}
|
||||
ranges[id] = rangeStatic;
|
||||
});
|
||||
|
||||
dispatch(rangeActions.setRanges(ranges));
|
||||
});
|
||||
@ -110,6 +104,8 @@ export const deleteRangeAndInsertThunk = createAsyncThunk(
|
||||
const { getState, dispatch } = thunkAPI;
|
||||
const state = getState() as RootState;
|
||||
const rangeState = state.documentRange;
|
||||
// if no range, just return
|
||||
if (rangeState.caret && rangeState.caret.length === 0) return;
|
||||
const actions = [];
|
||||
// get merge actions
|
||||
const mergeActions = getMergeEndDeltaToStartActionsByRange(state, controller, insertDelta);
|
||||
@ -153,11 +149,11 @@ export const deleteRangeAndInsertEnterThunk = createAsyncThunk(
|
||||
const rangeState = state.documentRange;
|
||||
const actions = [];
|
||||
|
||||
const { startDelta, endDelta, endNode, startNode } = getStartAndEndDeltaExpectRange(state) || {};
|
||||
const { startDelta, endDelta, endNode, startNode } = getStartAndEndExtentDelta(state) || {};
|
||||
if (!startDelta || !endDelta || !endNode || !startNode) return;
|
||||
|
||||
// get middle nodes
|
||||
const middleIds = getMiddleIdsByRange(rangeState, state.document);
|
||||
const middleIds = getMiddleIds(state.document, startNode.id, endNode.id);
|
||||
|
||||
let newStartDelta = new Delta(startDelta);
|
||||
let caret = null;
|
||||
|
@ -15,6 +15,8 @@ import { blockConfig } from '$app/constants/document/config';
|
||||
import {
|
||||
caretInBottomEdgeByDelta,
|
||||
caretInTopEdgeByDelta,
|
||||
getAfterExtentDeltaByRange,
|
||||
getBeofreExtentDeltaByRange,
|
||||
getDeltaText,
|
||||
getIndexRelativeEnter,
|
||||
getLastLineIndex,
|
||||
@ -22,25 +24,34 @@ import {
|
||||
transformIndexToPrevLine,
|
||||
} from '$app/utils/document/delta';
|
||||
|
||||
export function getMiddleIdsByRange(rangeState: RangeState, document: DocumentState) {
|
||||
const { anchor, focus } = rangeState;
|
||||
if (!anchor || !focus) return;
|
||||
if (anchor.id === focus.id) return;
|
||||
const isForward = anchor.point.y < focus.point.y;
|
||||
// get all ids between anchor and focus
|
||||
const amendIds = [];
|
||||
const startId = isForward ? anchor.id : focus.id;
|
||||
const endId = isForward ? focus.id : anchor.id;
|
||||
|
||||
export function getMiddleIds(document: DocumentState, startId: string, endId: string) {
|
||||
const middleIds = [];
|
||||
let currentId: string | undefined = startId;
|
||||
while (currentId && currentId !== endId) {
|
||||
const nextId = getNextLineId(document, currentId);
|
||||
if (nextId && nextId !== endId) {
|
||||
amendIds.push(nextId);
|
||||
middleIds.push(nextId);
|
||||
}
|
||||
currentId = nextId;
|
||||
}
|
||||
return amendIds;
|
||||
return middleIds;
|
||||
}
|
||||
|
||||
export function getStartAndEndIdsByRange(rangeState: RangeState) {
|
||||
const { anchor, focus } = rangeState;
|
||||
if (!anchor || !focus) return [];
|
||||
if (anchor.id === focus.id) return [anchor.id];
|
||||
const isForward = anchor.point.y < focus.point.y;
|
||||
const startId = isForward ? anchor.id : focus.id;
|
||||
const endId = isForward ? focus.id : anchor.id;
|
||||
return [startId, endId];
|
||||
}
|
||||
|
||||
export function getMiddleIdsByRange(rangeState: RangeState, document: DocumentState) {
|
||||
const ids = getStartAndEndIdsByRange(rangeState);
|
||||
if (ids.length < 2) return;
|
||||
const [startId, endId] = ids;
|
||||
return getMiddleIds(document, startId, endId);
|
||||
}
|
||||
|
||||
export function getAfterMergeCaretByRange(rangeState: RangeState, insertDelta?: Delta) {
|
||||
@ -61,42 +72,40 @@ export function getAfterMergeCaretByRange(rangeState: RangeState, insertDelta?:
|
||||
};
|
||||
}
|
||||
|
||||
export function getStartAndEndDeltaExpectRange(state: RootState) {
|
||||
export function getStartAndEndExtentDelta(state: RootState) {
|
||||
const rangeState = state.documentRange;
|
||||
const { anchor, focus, ranges } = rangeState;
|
||||
if (!anchor || !focus) return;
|
||||
if (anchor.id === focus.id) return;
|
||||
|
||||
const isForward = anchor.point.y < focus.point.y;
|
||||
const startId = isForward ? anchor.id : focus.id;
|
||||
const endId = isForward ? focus.id : anchor.id;
|
||||
|
||||
const ids = getStartAndEndIdsByRange(rangeState);
|
||||
if (ids.length === 0) return;
|
||||
const startId = ids[0];
|
||||
const endId = ids[ids.length - 1];
|
||||
const { ranges } = rangeState;
|
||||
// get start and end delta
|
||||
const startRange = ranges[startId];
|
||||
const endRange = ranges[endId];
|
||||
if (!startRange || !endRange) return;
|
||||
const startNode = state.document.nodes[startId];
|
||||
let startDelta = new Delta(startNode.data.delta);
|
||||
startDelta = startDelta.slice(0, startRange.index);
|
||||
const startNodeDelta = new Delta(startNode.data.delta);
|
||||
const startBeforeExtentDelta = getBeofreExtentDeltaByRange(startNodeDelta, startRange);
|
||||
|
||||
const endNode = state.document.nodes[endId];
|
||||
let endDelta = new Delta(endNode.data.delta);
|
||||
endDelta = endDelta.slice(endRange.index + endRange.length);
|
||||
const endNodeDelta = new Delta(endNode.data.delta);
|
||||
const endAfterExtentDelta = getAfterExtentDeltaByRange(endNodeDelta, endRange);
|
||||
|
||||
return {
|
||||
startNode,
|
||||
endNode,
|
||||
startDelta,
|
||||
endDelta,
|
||||
startDelta: startBeforeExtentDelta,
|
||||
endDelta: endAfterExtentDelta,
|
||||
};
|
||||
}
|
||||
|
||||
export function getMergeEndDeltaToStartActionsByRange(
|
||||
state: RootState,
|
||||
controller: DocumentController,
|
||||
insertDelta?: Delta
|
||||
) {
|
||||
const actions = [];
|
||||
const { startDelta, endDelta, endNode, startNode } = getStartAndEndDeltaExpectRange(state) || {};
|
||||
const { startDelta, endDelta, endNode, startNode } = getStartAndEndExtentDelta(state) || {};
|
||||
if (!startDelta || !endDelta || !endNode || !startNode) return;
|
||||
// merge start and end nodes
|
||||
const mergeDelta = startDelta.concat(insertDelta || new Delta()).concat(endDelta);
|
||||
@ -109,6 +118,14 @@ export function getMergeEndDeltaToStartActionsByRange(
|
||||
})
|
||||
);
|
||||
if (endNode.id !== startNode.id) {
|
||||
const children = state.document.children[endNode.children].map((id) => state.document.nodes[id]);
|
||||
|
||||
const moveChildrenActions = getMoveChildrenActions({
|
||||
target: startNode,
|
||||
children,
|
||||
controller,
|
||||
});
|
||||
actions.push(...moveChildrenActions);
|
||||
// delete end node
|
||||
actions.push(controller.getDeleteAction(endNode));
|
||||
}
|
||||
@ -116,6 +133,26 @@ export function getMergeEndDeltaToStartActionsByRange(
|
||||
return actions;
|
||||
}
|
||||
|
||||
export function getMoveChildrenActions({
|
||||
target,
|
||||
children,
|
||||
controller,
|
||||
prevId = '',
|
||||
}: {
|
||||
target: NestedBlock;
|
||||
children: NestedBlock[];
|
||||
controller: DocumentController;
|
||||
prevId?: string;
|
||||
}) {
|
||||
// move children
|
||||
const config = blockConfig[target.type];
|
||||
const targetParentId = config.canAddChild ? target.id : target.parent;
|
||||
if (!targetParentId) return [];
|
||||
const targetPrevId = targetParentId === target.id ? prevId : target.id;
|
||||
const moveActions = controller.getMoveChildrenAction(children, targetParentId, targetPrevId);
|
||||
return moveActions;
|
||||
}
|
||||
|
||||
export function getInsertEnterNodeFields(sourceNode: NestedBlock) {
|
||||
if (!sourceNode.parent) return;
|
||||
const parentId = sourceNode.parent;
|
||||
|
@ -0,0 +1,82 @@
|
||||
import { BlockData, DocumentBlockJSON, DocumentState, NestedBlock, RangeState } from '$app/interfaces/document';
|
||||
import { getDeltaByRange } from '$app/utils/document/delta';
|
||||
import Delta from 'quill-delta';
|
||||
import { generateId } from '$app/utils/document/block';
|
||||
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
||||
import { blockConfig } from '$app/constants/document/config';
|
||||
|
||||
export function getCopyData(
|
||||
node: NestedBlock,
|
||||
range: {
|
||||
index: number;
|
||||
length: number;
|
||||
}
|
||||
): BlockData<any> {
|
||||
const nodeDeltaOps = node.data.delta;
|
||||
if (!nodeDeltaOps) {
|
||||
return {
|
||||
...node.data,
|
||||
};
|
||||
}
|
||||
const delta = getDeltaByRange(new Delta(node.data.delta), range);
|
||||
return {
|
||||
...node.data,
|
||||
delta: delta.ops,
|
||||
};
|
||||
}
|
||||
|
||||
export function getCopyBlock(id: string, document: DocumentState, documentRange: RangeState): DocumentBlockJSON {
|
||||
const node = document.nodes[id];
|
||||
const range = documentRange.ranges[id] || { index: 0, length: 0 };
|
||||
const copyData = getCopyData(node, range);
|
||||
return {
|
||||
type: node.type,
|
||||
data: copyData,
|
||||
children: [],
|
||||
};
|
||||
}
|
||||
|
||||
export function generateBlocks(data: DocumentBlockJSON[], parentId: string) {
|
||||
const blocks: NestedBlock[] = [];
|
||||
function dfs(data: DocumentBlockJSON[], parentId: string) {
|
||||
data.forEach((item) => {
|
||||
const block = {
|
||||
id: generateId(),
|
||||
type: item.type,
|
||||
data: item.data,
|
||||
parent: parentId,
|
||||
children: generateId(),
|
||||
};
|
||||
blocks.push(block);
|
||||
if (item.children) {
|
||||
dfs(item.children, block.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
dfs(data, parentId);
|
||||
return blocks;
|
||||
}
|
||||
|
||||
export function getInsertBlockActions(blocks: NestedBlock[], prevId: string, controller: DocumentController) {
|
||||
return blocks.map((block, index) => {
|
||||
const prevBlockId = index === 0 ? prevId : blocks[index - 1].id;
|
||||
return controller.getInsertAction(block, prevBlockId);
|
||||
});
|
||||
}
|
||||
|
||||
export function getAppendBlockDeltaAction(
|
||||
block: NestedBlock,
|
||||
appendDelta: Delta,
|
||||
isForward: boolean,
|
||||
controller: DocumentController
|
||||
) {
|
||||
const nodeDelta = new Delta(block.data.delta);
|
||||
const mergeDelta = isForward ? appendDelta.concat(nodeDelta) : nodeDelta.concat(appendDelta);
|
||||
return controller.getUpdateAction({
|
||||
...block,
|
||||
data: {
|
||||
...block.data,
|
||||
delta: mergeDelta.ops,
|
||||
},
|
||||
});
|
||||
}
|
@ -1,10 +1,10 @@
|
||||
import Delta from "quill-delta";
|
||||
import Delta from 'quill-delta';
|
||||
|
||||
export function getDeltaText(delta: Delta) {
|
||||
const text = delta
|
||||
.filter((op) => typeof op.insert === "string")
|
||||
.filter((op) => typeof op.insert === 'string')
|
||||
.map((op) => op.insert)
|
||||
.join("");
|
||||
.join('');
|
||||
return text;
|
||||
}
|
||||
|
||||
@ -12,7 +12,7 @@ export function caretInTopEdgeByDelta(delta: Delta, index: number) {
|
||||
const text = getDeltaText(delta.slice(0, index));
|
||||
if (!text) return true;
|
||||
|
||||
const firstLine = text.split("\n")[0];
|
||||
const firstLine = text.split('\n')[0];
|
||||
return index <= firstLine.length;
|
||||
}
|
||||
|
||||
@ -20,14 +20,14 @@ export function caretInBottomEdgeByDelta(delta: Delta, index: number) {
|
||||
const text = getDeltaText(delta.slice(index));
|
||||
|
||||
if (!text) return true;
|
||||
return !text.includes("\n");
|
||||
return !text.includes('\n');
|
||||
}
|
||||
|
||||
export function getLineByIndex(delta: Delta, index: number) {
|
||||
const beforeText = getDeltaText(delta.slice(0, index));
|
||||
const afterText = getDeltaText(delta.slice(index));
|
||||
const beforeLines = beforeText.split("\n");
|
||||
const afterLines = afterText.split("\n");
|
||||
const beforeLines = beforeText.split('\n');
|
||||
const afterLines = afterText.split('\n');
|
||||
|
||||
const startLineText = beforeLines[beforeLines.length - 1];
|
||||
const currentLineText = startLineText + afterLines[0];
|
||||
@ -39,7 +39,7 @@ export function getLineByIndex(delta: Delta, index: number) {
|
||||
|
||||
export function transformIndexToPrevLine(delta: Delta, index: number) {
|
||||
const text = getDeltaText(delta.slice(0, index));
|
||||
const lines = text.split("\n");
|
||||
const lines = text.split('\n');
|
||||
if (lines.length < 2) return 0;
|
||||
const prevLineText = lines[lines.length - 2];
|
||||
const transformedIndex = index - prevLineText.length - 1;
|
||||
@ -59,13 +59,47 @@ export function transformIndexToNextLine(delta: Delta, index: number) {
|
||||
|
||||
export function getIndexRelativeEnter(delta: Delta, index: number) {
|
||||
const text = getDeltaText(delta.slice(0, index));
|
||||
const beforeLines = text.split("\n");
|
||||
const beforeLines = text.split('\n');
|
||||
const beforeLineText = beforeLines[beforeLines.length - 1];
|
||||
return beforeLineText.length;
|
||||
}
|
||||
|
||||
export function getLastLineIndex(delta: Delta) {
|
||||
const text = getDeltaText(delta);
|
||||
const lastIndex = text.lastIndexOf("\n");
|
||||
const lastIndex = text.lastIndexOf('\n');
|
||||
return lastIndex === -1 ? 0 : lastIndex + 1;
|
||||
}
|
||||
}
|
||||
|
||||
export function getDeltaByRange(
|
||||
delta: Delta,
|
||||
range: {
|
||||
index: number;
|
||||
length: number;
|
||||
}
|
||||
) {
|
||||
const start = range.index;
|
||||
const end = range.index + range.length;
|
||||
return new Delta(delta.slice(start, end));
|
||||
}
|
||||
|
||||
export function getBeofreExtentDeltaByRange(
|
||||
delta: Delta,
|
||||
range: {
|
||||
index: number;
|
||||
length: number;
|
||||
}
|
||||
) {
|
||||
const start = range.index;
|
||||
return new Delta(delta.slice(0, start));
|
||||
}
|
||||
|
||||
export function getAfterExtentDeltaByRange(
|
||||
delta: Delta,
|
||||
range: {
|
||||
index: number;
|
||||
length: number;
|
||||
}
|
||||
) {
|
||||
const start = range.index + range.length;
|
||||
return new Delta(delta.slice(start));
|
||||
}
|
||||
|
@ -0,0 +1,28 @@
|
||||
import isHotkey from 'is-hotkey';
|
||||
import { Keyboard } from '$app/constants/document/keyboard';
|
||||
import { TextAction } from '$app/interfaces/document';
|
||||
|
||||
export function isFormatHotkey(e: KeyboardEvent | React.KeyboardEvent<HTMLDivElement>) {
|
||||
return (
|
||||
isHotkey(Keyboard.keys.FORMAT.BOLD, e) ||
|
||||
isHotkey(Keyboard.keys.FORMAT.ITALIC, e) ||
|
||||
isHotkey(Keyboard.keys.FORMAT.UNDERLINE, e) ||
|
||||
isHotkey(Keyboard.keys.FORMAT.STRIKE, e) ||
|
||||
isHotkey(Keyboard.keys.FORMAT.CODE, e)
|
||||
);
|
||||
}
|
||||
|
||||
export function parseFormat(e: KeyboardEvent | React.KeyboardEvent<HTMLDivElement>) {
|
||||
if (isHotkey(Keyboard.keys.FORMAT.BOLD, e)) {
|
||||
return TextAction.Bold;
|
||||
} else if (isHotkey(Keyboard.keys.FORMAT.ITALIC, e)) {
|
||||
return TextAction.Italic;
|
||||
} else if (isHotkey(Keyboard.keys.FORMAT.UNDERLINE, e)) {
|
||||
return TextAction.Underline;
|
||||
} else if (isHotkey(Keyboard.keys.FORMAT.STRIKE, e)) {
|
||||
return TextAction.Strikethrough;
|
||||
} else if (isHotkey(Keyboard.keys.FORMAT.CODE, e)) {
|
||||
return TextAction.Code;
|
||||
}
|
||||
return null;
|
||||
}
|
@ -145,7 +145,7 @@ export function isPointInBlock(target: HTMLElement | null) {
|
||||
|
||||
export function findTextNode(
|
||||
node: Element,
|
||||
index: number,
|
||||
index: number
|
||||
): {
|
||||
node?: Node;
|
||||
offset?: number;
|
||||
@ -191,7 +191,6 @@ export function focusNodeByIndex(node: Element, index: number, length: number) {
|
||||
selection?.addRange(range);
|
||||
}
|
||||
|
||||
|
||||
export function getNodeTextBoxByBlockId(blockId: string) {
|
||||
const node = getNode(blockId);
|
||||
return node?.querySelector(`[role="textbox"]`);
|
||||
@ -229,4 +228,4 @@ export function findParent(node: Element, parentSelector: string) {
|
||||
parentNode = parentNode.parentElement;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user