mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-07 09:21:24 +08:00
@@ -905,6 +905,19 @@ class SlateUtils {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public insertNodes(
|
||||||
|
nodes: SlateNode | Array<SlateNode>,
|
||||||
|
options?: Parameters<typeof Transforms.insertNodes>[2]
|
||||||
|
) {
|
||||||
|
Transforms.insertNodes(this.editor, nodes, {
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public getNodeByPath(path: Path) {
|
||||||
|
Editor.node(this.editor, path);
|
||||||
|
}
|
||||||
|
|
||||||
public getStartSelection() {
|
public getStartSelection() {
|
||||||
return {
|
return {
|
||||||
anchor: this.getStart(),
|
anchor: this.getStart(),
|
||||||
|
|||||||
@@ -45,13 +45,13 @@ const TextBlockContainer = styled(Text)(({ theme }) => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const findSlice = (arr: string[], p: string, q: string) => {
|
const findSlice = (arr: string[], p: string, q: string) => {
|
||||||
let should_include = false;
|
let shouldInclude = false;
|
||||||
return arr.filter(block => {
|
return arr.filter(block => {
|
||||||
if (block === p || block === q) {
|
if (block === p || block === q) {
|
||||||
should_include = !should_include;
|
shouldInclude = !shouldInclude;
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
return should_include;
|
return shouldInclude;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -115,11 +115,8 @@ export const TextManage = forwardRef<ExtendedTextUtils, CreateTextView>(
|
|||||||
(ref as MutableRefObject<ExtendedTextUtils>) || defaultRef;
|
(ref as MutableRefObject<ExtendedTextUtils>) || defaultRef;
|
||||||
|
|
||||||
const properties = block.getProperties();
|
const properties = block.getProperties();
|
||||||
// const [is_select, set_is_select] = useState<boolean>();
|
|
||||||
// useOnSelect(block.id, (is_select: boolean) => {
|
const onTextViewSetSelection = (selection: Range | Point) => {
|
||||||
// set_is_select(is_select);
|
|
||||||
// });
|
|
||||||
const on_text_view_set_selection = (selection: Range | Point) => {
|
|
||||||
if (selection instanceof Point) {
|
if (selection instanceof Point) {
|
||||||
//do some thing
|
//do some thing
|
||||||
} else {
|
} else {
|
||||||
@@ -128,18 +125,18 @@ export const TextManage = forwardRef<ExtendedTextUtils, CreateTextView>(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// block = await editor.commands.blockCommands.createNextBlock(block.id,)
|
// block = await editor.commands.blockCommands.createNextBlock(block.id,)
|
||||||
const on_text_view_active = useCallback(
|
const onTextViewActive = useCallback(
|
||||||
(point: CursorTypes) => {
|
(point: CursorTypes) => {
|
||||||
// TODO code to be optimized
|
// TODO code to be optimized
|
||||||
if (textRef.current) {
|
if (textRef.current) {
|
||||||
const end_selection = textRef.current.getEndSelection();
|
const endSelection = textRef.current.getEndSelection();
|
||||||
const start_selection = textRef.current.getStartSelection();
|
const startSelection = textRef.current.getStartSelection();
|
||||||
if (point === 'start') {
|
if (point === 'start') {
|
||||||
textRef.current.setSelection(start_selection);
|
textRef.current.setSelection(startSelection);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (point === 'end') {
|
if (point === 'end') {
|
||||||
textRef.current.setSelection(end_selection);
|
textRef.current.setSelection(endSelection);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -154,24 +151,24 @@ export const TextManage = forwardRef<ExtendedTextUtils, CreateTextView>(
|
|||||||
} else {
|
} else {
|
||||||
blockTop = blockDomStyle.top + 5;
|
blockTop = blockDomStyle.top + 5;
|
||||||
}
|
}
|
||||||
const end_position = ReactEditor.toDOMRange(
|
const endPosition = ReactEditor.toDOMRange(
|
||||||
textRef.current.editor,
|
textRef.current.editor,
|
||||||
end_selection
|
endSelection
|
||||||
)
|
)
|
||||||
.getClientRects()
|
.getClientRects()
|
||||||
.item(0);
|
.item(0);
|
||||||
const start_position = ReactEditor.toDOMRange(
|
const startPosition = ReactEditor.toDOMRange(
|
||||||
textRef.current.editor,
|
textRef.current.editor,
|
||||||
start_selection
|
startSelection
|
||||||
)
|
)
|
||||||
.getClientRects()
|
.getClientRects()
|
||||||
.item(0);
|
.item(0);
|
||||||
if (end_position.left <= point.x) {
|
if (endPosition.left <= point.x) {
|
||||||
textRef.current.setSelection(end_selection);
|
textRef.current.setSelection(endSelection);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (start_position.left >= point.x) {
|
if (startPosition.left >= point.x) {
|
||||||
textRef.current.setSelection(start_selection);
|
textRef.current.setSelection(startSelection);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let range: globalThis.Range;
|
let range: globalThis.Range;
|
||||||
@@ -189,7 +186,7 @@ export const TextManage = forwardRef<ExtendedTextUtils, CreateTextView>(
|
|||||||
range = document.createRange();
|
range = document.createRange();
|
||||||
range.setStart(caret.offsetNode, caret.offset);
|
range.setStart(caret.offsetNode, caret.offset);
|
||||||
}
|
}
|
||||||
const slate_rang = ReactEditor.toSlateRange(
|
const slateRang = ReactEditor.toSlateRange(
|
||||||
textRef.current.editor,
|
textRef.current.editor,
|
||||||
range,
|
range,
|
||||||
{
|
{
|
||||||
@@ -197,19 +194,19 @@ export const TextManage = forwardRef<ExtendedTextUtils, CreateTextView>(
|
|||||||
suppressThrow: true,
|
suppressThrow: true,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
textRef.current.setSelection(slate_rang);
|
textRef.current.setSelection(slateRang);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('e: ', e);
|
console.log('e: ', e);
|
||||||
textRef.current.setSelection(end_selection);
|
textRef.current.setSelection(endSelection);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[textRef]
|
[textRef]
|
||||||
);
|
);
|
||||||
|
|
||||||
useOnSelectActive(block.id, on_text_view_active);
|
useOnSelectActive(block.id, onTextViewActive);
|
||||||
useOnSelectSetSelection<'Range'>(block.id, on_text_view_set_selection);
|
useOnSelectSetSelection<'Range'>(block.id, onTextViewSetSelection);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (textRef.current) {
|
if (textRef.current) {
|
||||||
@@ -235,17 +232,17 @@ export const TextManage = forwardRef<ExtendedTextUtils, CreateTextView>(
|
|||||||
(block.id === lastSelectNodeId && type === 'Range') ||
|
(block.id === lastSelectNodeId && type === 'Range') ||
|
||||||
(type === 'Range' && info)
|
(type === 'Range' && info)
|
||||||
) {
|
) {
|
||||||
on_text_view_active('end');
|
onTextViewActive('end');
|
||||||
} else {
|
} else {
|
||||||
on_text_view_active('start');
|
onTextViewActive('start');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('error occured in set active in initialization');
|
console.warn('error occured in set active in initialization');
|
||||||
}
|
}
|
||||||
}, [block.id, editor.selectionManager, on_text_view_active, textRef]);
|
}, [block.id, editor.selectionManager, onTextViewActive, textRef]);
|
||||||
|
|
||||||
const on_text_change: TextProps['handleChange'] = async (
|
const onTextChange: TextProps['handleChange'] = async (
|
||||||
value,
|
value,
|
||||||
textStyle
|
textStyle
|
||||||
) => {
|
) => {
|
||||||
@@ -266,39 +263,34 @@ export const TextManage = forwardRef<ExtendedTextUtils, CreateTextView>(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const get_now_and_pre_rang_position = () => {
|
const getNowAndPreRangPosition = () => {
|
||||||
window.getSelection().getRangeAt(0);
|
const nowRange = window.getSelection().getRangeAt(0);
|
||||||
// const now_range =
|
let prePosition = null;
|
||||||
// editor.selectionManager.currentSelectInfo?.browserSelection.getRangeAt(
|
const nowPosition = nowRange.getClientRects().item(0);
|
||||||
// 0
|
|
||||||
// );
|
|
||||||
const now_range = window.getSelection().getRangeAt(0);
|
|
||||||
let pre_position = null;
|
|
||||||
const now_position = now_range.getClientRects().item(0);
|
|
||||||
try {
|
try {
|
||||||
if (now_range.startOffset !== 0) {
|
if (nowRange.startOffset !== 0) {
|
||||||
const pre_rang = document.createRange();
|
const preRang = document.createRange();
|
||||||
pre_rang.setStart(
|
preRang.setStart(
|
||||||
now_range.startContainer,
|
nowRange.startContainer,
|
||||||
now_range.startOffset + 1
|
nowRange.startOffset + 1
|
||||||
);
|
);
|
||||||
pre_rang.setEnd(
|
preRang.setEnd(
|
||||||
now_range.endContainer,
|
nowRange.endContainer,
|
||||||
now_range.endOffset + 1
|
nowRange.endOffset + 1
|
||||||
);
|
);
|
||||||
pre_position = pre_rang.getClientRects().item(0);
|
prePosition = preRang.getClientRects().item(0);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// console.log(e);
|
// console.log(e);
|
||||||
}
|
}
|
||||||
return { nowPosition: now_position, prePosition: pre_position };
|
return { nowPosition: nowPosition, prePosition: prePosition };
|
||||||
};
|
};
|
||||||
|
|
||||||
const onKeyboardUp = (event: React.KeyboardEvent<Element>) => {
|
const onKeyboardUp = (event: React.KeyboardEvent<Element>) => {
|
||||||
// if default event is prevented do noting
|
// if default event is prevented do noting
|
||||||
// if U want to disable up/down/enter use capture event for preventing
|
// if U want to disable up/down/enter use capture event for preventing
|
||||||
if (!event.isDefaultPrevented()) {
|
if (!event.isDefaultPrevented()) {
|
||||||
const positions = get_now_and_pre_rang_position();
|
const positions = getNowAndPreRangPosition();
|
||||||
const prePosition = positions.prePosition;
|
const prePosition = positions.prePosition;
|
||||||
const nowPosition = positions.nowPosition;
|
const nowPosition = positions.nowPosition;
|
||||||
if (prePosition) {
|
if (prePosition) {
|
||||||
@@ -339,7 +331,7 @@ export const TextManage = forwardRef<ExtendedTextUtils, CreateTextView>(
|
|||||||
// editor.selectionManager.activeNextNode(block.id, 'start');
|
// editor.selectionManager.activeNextNode(block.id, 'start');
|
||||||
// return;
|
// return;
|
||||||
if (!event.isDefaultPrevented()) {
|
if (!event.isDefaultPrevented()) {
|
||||||
const positions = get_now_and_pre_rang_position();
|
const positions = getNowAndPreRangPosition();
|
||||||
const prePosition = positions.prePosition;
|
const prePosition = positions.prePosition;
|
||||||
const nowPosition = positions.nowPosition;
|
const nowPosition = positions.nowPosition;
|
||||||
// Create the last element range of slate_editor
|
// Create the last element range of slate_editor
|
||||||
@@ -395,7 +387,7 @@ export const TextManage = forwardRef<ExtendedTextUtils, CreateTextView>(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const on_select_all = () => {
|
const onSelectAll = () => {
|
||||||
const isSelectAll =
|
const isSelectAll =
|
||||||
textRef.current.isEmpty() || textRef.current.isSelectAll();
|
textRef.current.isEmpty() || textRef.current.isSelectAll();
|
||||||
if (isSelectAll) {
|
if (isSelectAll) {
|
||||||
@@ -405,22 +397,20 @@ export const TextManage = forwardRef<ExtendedTextUtils, CreateTextView>(
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const on_undo = () => {
|
const onUndo = () => {
|
||||||
editor.undo();
|
editor.undo();
|
||||||
};
|
};
|
||||||
|
|
||||||
const on_redo = () => {
|
const onRedo = () => {
|
||||||
editor.redo();
|
editor.redo();
|
||||||
};
|
};
|
||||||
|
|
||||||
const on_keyboard_esc = () => {
|
const onKeyboardEsc = () => {
|
||||||
if (editor.selectionManager.getSelectedNodesIds().length === 0) {
|
if (editor.selectionManager.getSelectedNodesIds().length === 0) {
|
||||||
const active_node_id =
|
const activeNodeId =
|
||||||
editor.selectionManager.getActivatedNodeId();
|
editor.selectionManager.getActivatedNodeId();
|
||||||
if (active_node_id) {
|
if (activeNodeId) {
|
||||||
editor.selectionManager.setSelectedNodesIds([
|
editor.selectionManager.setSelectedNodesIds([activeNodeId]);
|
||||||
active_node_id,
|
|
||||||
]);
|
|
||||||
ReactEditor.blur(textRef.current.editor);
|
ReactEditor.blur(textRef.current.editor);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -428,7 +418,7 @@ export const TextManage = forwardRef<ExtendedTextUtils, CreateTextView>(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const on_shift_click = async (e: MouseEvent) => {
|
const onShiftClick = async (e: MouseEvent) => {
|
||||||
if (e.shiftKey) {
|
if (e.shiftKey) {
|
||||||
const activeId = editor.selectionManager.getActivatedNodeId();
|
const activeId = editor.selectionManager.getActivatedNodeId();
|
||||||
if (activeId === block.id) {
|
if (activeId === block.id) {
|
||||||
@@ -477,16 +467,16 @@ export const TextManage = forwardRef<ExtendedTextUtils, CreateTextView>(
|
|||||||
className={`${otherOptions.className}`}
|
className={`${otherOptions.className}`}
|
||||||
currentValue={properties.text.value}
|
currentValue={properties.text.value}
|
||||||
textStyle={properties.textStyle}
|
textStyle={properties.textStyle}
|
||||||
handleChange={on_text_change}
|
handleChange={onTextChange}
|
||||||
handleUp={onKeyboardUp}
|
handleUp={onKeyboardUp}
|
||||||
handleDown={onKeyboardDown}
|
handleDown={onKeyboardDown}
|
||||||
handleLeft={onKeyboardLeft}
|
handleLeft={onKeyboardLeft}
|
||||||
handleRight={onKeyboardRight}
|
handleRight={onKeyboardRight}
|
||||||
handleSelectAll={on_select_all}
|
handleSelectAll={onSelectAll}
|
||||||
handleMouseDown={on_shift_click}
|
handleMouseDown={onShiftClick}
|
||||||
handleUndo={on_undo}
|
handleUndo={onUndo}
|
||||||
handleRedo={on_redo}
|
handleRedo={onRedo}
|
||||||
handleEsc={on_keyboard_esc}
|
handleEsc={onKeyboardEsc}
|
||||||
{...otherOptions}
|
{...otherOptions}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,7 +3,13 @@ import type {
|
|||||||
SlateUtils,
|
SlateUtils,
|
||||||
TextAlignOptions,
|
TextAlignOptions,
|
||||||
} from '@toeverything/components/common';
|
} from '@toeverything/components/common';
|
||||||
import { Point, Selection as SlateSelection } from 'slate';
|
import {
|
||||||
|
BaseRange,
|
||||||
|
Node,
|
||||||
|
Path,
|
||||||
|
Point,
|
||||||
|
Selection as SlateSelection,
|
||||||
|
} from 'slate';
|
||||||
import { Editor } from '../editor';
|
import { Editor } from '../editor';
|
||||||
|
|
||||||
type TextUtilsFunctions =
|
type TextUtilsFunctions =
|
||||||
@@ -28,7 +34,10 @@ type TextUtilsFunctions =
|
|||||||
| 'removeSelection'
|
| 'removeSelection'
|
||||||
| 'insertReference'
|
| 'insertReference'
|
||||||
| 'isCollapsed'
|
| 'isCollapsed'
|
||||||
| 'blur';
|
| 'blur'
|
||||||
|
| 'setSelection'
|
||||||
|
| 'insertNodes'
|
||||||
|
| 'getNodeByPath';
|
||||||
|
|
||||||
type ExtendedTextUtils = SlateUtils & {
|
type ExtendedTextUtils = SlateUtils & {
|
||||||
setLinkModalVisible: (visible: boolean) => void;
|
setLinkModalVisible: (visible: boolean) => void;
|
||||||
@@ -193,6 +202,59 @@ export class BlockHelper {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* set selection of a text input
|
||||||
|
* @param {string} blockId
|
||||||
|
* @param {BaseRange} selection
|
||||||
|
* @return {*}
|
||||||
|
* @memberof BlockHelper
|
||||||
|
*/
|
||||||
|
public setSelection(blockId: string, selection: BaseRange) {
|
||||||
|
const text_utils = this._blockTextUtilsMap[blockId];
|
||||||
|
if (text_utils) {
|
||||||
|
return text_utils.setSelection(selection);
|
||||||
|
}
|
||||||
|
console.warn('Could find the block text utils');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* insert nodes in text
|
||||||
|
* @param {string} blockId
|
||||||
|
* @param {Array<Node>} nodes
|
||||||
|
* @param {Parameters<TextUtils['insertNodes']>[1]} options
|
||||||
|
* @return {*}
|
||||||
|
* @memberof BlockHelper
|
||||||
|
*/
|
||||||
|
public insertNodes(
|
||||||
|
blockId: string,
|
||||||
|
nodes: Array<Node>,
|
||||||
|
options?: Parameters<TextUtils['insertNodes']>[1]
|
||||||
|
) {
|
||||||
|
const text_utils = this._blockTextUtilsMap[blockId];
|
||||||
|
if (text_utils) {
|
||||||
|
return text_utils.insertNodes(nodes, options);
|
||||||
|
}
|
||||||
|
console.warn('Could find the block text utils');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* get text(slate node) by path
|
||||||
|
* @param {string} blockId
|
||||||
|
* @param {Path} path
|
||||||
|
* @return {*}
|
||||||
|
* @memberof BlockHelper
|
||||||
|
*/
|
||||||
|
public getNodeByPath(blockId: string, path: Path) {
|
||||||
|
const text_utils = this._blockTextUtilsMap[blockId];
|
||||||
|
if (text_utils) {
|
||||||
|
return text_utils.getNodeByPath(path);
|
||||||
|
}
|
||||||
|
console.warn('Could find the block text utils');
|
||||||
|
}
|
||||||
|
|
||||||
public transformPoint(
|
public transformPoint(
|
||||||
blockId: string,
|
blockId: string,
|
||||||
...restArgs: Parameters<TextUtils['transformPoint']>
|
...restArgs: Parameters<TextUtils['transformPoint']>
|
||||||
|
|||||||
@@ -14,403 +14,126 @@ import {
|
|||||||
services,
|
services,
|
||||||
} from '@toeverything/datasource/db-service';
|
} from '@toeverything/datasource/db-service';
|
||||||
import { MarkdownParser } from './markdown-parse';
|
import { MarkdownParser } from './markdown-parse';
|
||||||
|
import { shouldHandlerContinue } from './utils';
|
||||||
|
import { Paste } from './paste';
|
||||||
// todo needs to be a switch
|
// todo needs to be a switch
|
||||||
const support_markdown_paste = true;
|
|
||||||
|
|
||||||
enum ClipboardAction {
|
enum ClipboardAction {
|
||||||
COPY = 'copy',
|
COPY = 'copy',
|
||||||
CUT = 'cut',
|
CUT = 'cut',
|
||||||
PASTE = 'paste',
|
PASTE = 'paste',
|
||||||
}
|
}
|
||||||
class BrowserClipboard {
|
|
||||||
private event_target: Element;
|
|
||||||
private hooks: HooksRunner;
|
|
||||||
private editor: Editor;
|
|
||||||
private clipboard_parse: ClipboardParse;
|
|
||||||
private markdown_parse: MarkdownParser;
|
|
||||||
|
|
||||||
private static optimal_mime_type: string[] = [
|
//TODO: need to consider the cursor position after inserting the children
|
||||||
OFFICE_CLIPBOARD_MIMETYPE.DOCS_DOCUMENT_SLICE_CLIP_WRAPPED,
|
class BrowserClipboard {
|
||||||
OFFICE_CLIPBOARD_MIMETYPE.HTML,
|
private _eventTarget: Element;
|
||||||
OFFICE_CLIPBOARD_MIMETYPE.TEXT,
|
private _hooks: HooksRunner;
|
||||||
];
|
private _editor: Editor;
|
||||||
|
private _clipboardParse: ClipboardParse;
|
||||||
|
private _markdownParse: MarkdownParser;
|
||||||
|
private _paste: Paste;
|
||||||
|
|
||||||
constructor(eventTarget: Element, hooks: HooksRunner, editor: Editor) {
|
constructor(eventTarget: Element, hooks: HooksRunner, editor: Editor) {
|
||||||
this.event_target = eventTarget;
|
this._eventTarget = eventTarget;
|
||||||
this.hooks = hooks;
|
this._hooks = hooks;
|
||||||
this.editor = editor;
|
this._editor = editor;
|
||||||
this.clipboard_parse = new ClipboardParse(editor);
|
this._clipboardParse = new ClipboardParse(editor);
|
||||||
this.markdown_parse = new MarkdownParser();
|
this._markdownParse = new MarkdownParser();
|
||||||
this.initialize();
|
this._paste = new Paste(
|
||||||
|
editor,
|
||||||
|
this._clipboardParse,
|
||||||
|
this._markdownParse
|
||||||
|
);
|
||||||
|
this._initialize();
|
||||||
}
|
}
|
||||||
|
|
||||||
public getClipboardParse() {
|
public getClipboardParse() {
|
||||||
return this.clipboard_parse;
|
return this._clipboardParse;
|
||||||
}
|
}
|
||||||
|
|
||||||
private initialize() {
|
private _initialize() {
|
||||||
this.handle_copy = this.handle_copy.bind(this);
|
this._handleCopy = this._handleCopy.bind(this);
|
||||||
this.handle_cut = this.handle_cut.bind(this);
|
this._handleCut = this._handleCut.bind(this);
|
||||||
this.handle_paste = this.handle_paste.bind(this);
|
|
||||||
|
|
||||||
document.addEventListener(ClipboardAction.COPY, this.handle_copy);
|
document.addEventListener(ClipboardAction.COPY, this._handleCopy);
|
||||||
document.addEventListener(ClipboardAction.CUT, this.handle_cut);
|
document.addEventListener(ClipboardAction.CUT, this._handleCut);
|
||||||
document.addEventListener(ClipboardAction.PASTE, this.handle_paste);
|
document.addEventListener(
|
||||||
this.event_target.addEventListener(
|
|
||||||
ClipboardAction.COPY,
|
|
||||||
this.handle_copy
|
|
||||||
);
|
|
||||||
this.event_target.addEventListener(
|
|
||||||
ClipboardAction.CUT,
|
|
||||||
this.handle_cut
|
|
||||||
);
|
|
||||||
this.event_target.addEventListener(
|
|
||||||
ClipboardAction.PASTE,
|
ClipboardAction.PASTE,
|
||||||
this.handle_paste
|
this._paste.handlePaste
|
||||||
);
|
);
|
||||||
}
|
this._eventTarget.addEventListener(
|
||||||
|
|
||||||
private handle_copy(e: Event) {
|
|
||||||
//@ts-ignore
|
|
||||||
if (e.defaultPrevented || e.target.nodeName === 'INPUT') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.dispatch_clipboard_event(
|
|
||||||
ClipboardAction.COPY,
|
ClipboardAction.COPY,
|
||||||
e as ClipboardEvent
|
this._handleCopy
|
||||||
|
);
|
||||||
|
this._eventTarget.addEventListener(
|
||||||
|
ClipboardAction.CUT,
|
||||||
|
this._handleCut
|
||||||
|
);
|
||||||
|
this._eventTarget.addEventListener(
|
||||||
|
ClipboardAction.PASTE,
|
||||||
|
this._paste.handlePaste
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private handle_cut(e: Event) {
|
private _handleCopy(e: Event) {
|
||||||
//@ts-ignore
|
if (!shouldHandlerContinue(e, this._editor)) {
|
||||||
if (e.defaultPrevented || e.target.nodeName === 'INPUT') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.dispatch_clipboard_event(ClipboardAction.CUT, e as ClipboardEvent);
|
|
||||||
}
|
|
||||||
|
|
||||||
private handle_paste(e: Event) {
|
|
||||||
//@ts-ignore TODO should be handled more scientifically here, whether to trigger the paste time, also need some whitelist mechanism
|
|
||||||
if (e.defaultPrevented || e.target.nodeName === 'INPUT') {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const clipboardData = (e as ClipboardEvent).clipboardData;
|
this._dispatchClipboardEvent(ClipboardAction.COPY, e as ClipboardEvent);
|
||||||
|
|
||||||
const isPureFile = this.is_pure_file_in_clipboard(clipboardData);
|
|
||||||
|
|
||||||
if (!isPureFile) {
|
|
||||||
this.paste_content(clipboardData);
|
|
||||||
} else {
|
|
||||||
this.paste_file(clipboardData);
|
|
||||||
}
|
|
||||||
// this.editor.selectionManager
|
|
||||||
// .getSelectInfo()
|
|
||||||
// .then(selectionInfo => console.log(selectionInfo));
|
|
||||||
|
|
||||||
e.stopPropagation();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private paste_content(clipboardData: any) {
|
private _handleCut(e: Event) {
|
||||||
const originClip: { data: any; type: any } = this.getOptimalClip(
|
if (!shouldHandlerContinue(e, this._editor)) {
|
||||||
clipboardData
|
|
||||||
) as { data: any; type: any };
|
|
||||||
const originTextClipData = clipboardData.getData(
|
|
||||||
OFFICE_CLIPBOARD_MIMETYPE.TEXT
|
|
||||||
);
|
|
||||||
|
|
||||||
let clipData = originClip['data'];
|
|
||||||
|
|
||||||
if (originClip['type'] === OFFICE_CLIPBOARD_MIMETYPE.TEXT) {
|
|
||||||
clipData = this.excape_html(clipData);
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (originClip['type']) {
|
|
||||||
/** Protocol paste */
|
|
||||||
case OFFICE_CLIPBOARD_MIMETYPE.DOCS_DOCUMENT_SLICE_CLIP_WRAPPED:
|
|
||||||
this.fire_paste_edit_action(clipData);
|
|
||||||
break;
|
|
||||||
case OFFICE_CLIPBOARD_MIMETYPE.HTML:
|
|
||||||
this.paste_html(clipData, originTextClipData);
|
|
||||||
break;
|
|
||||||
case OFFICE_CLIPBOARD_MIMETYPE.TEXT:
|
|
||||||
this.paste_text(clipData, originTextClipData);
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private paste_html(clipData: any, originTextClipData: any) {
|
|
||||||
if (support_markdown_paste) {
|
|
||||||
const has_markdown =
|
|
||||||
this.markdown_parse.checkIfTextContainsMd(originTextClipData);
|
|
||||||
if (has_markdown) {
|
|
||||||
try {
|
|
||||||
const convertedDataObj =
|
|
||||||
this.markdown_parse.md2Html(originTextClipData);
|
|
||||||
if (convertedDataObj.isConverted) {
|
|
||||||
clipData = convertedDataObj.text;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
clipData = originTextClipData;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const blocks = this.clipboard_parse.html2blocks(clipData);
|
|
||||||
this.insert_blocks(blocks);
|
|
||||||
}
|
|
||||||
|
|
||||||
private paste_text(clipData: any, originTextClipData: any) {
|
|
||||||
const blocks = this.clipboard_parse.text2blocks(clipData);
|
|
||||||
this.insert_blocks(blocks);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async paste_file(clipboardData: any) {
|
|
||||||
const file = this.get_image_file(clipboardData);
|
|
||||||
if (file) {
|
|
||||||
const result = await services.api.file.create({
|
|
||||||
workspace: this.editor.workspace,
|
|
||||||
file: file,
|
|
||||||
});
|
|
||||||
const block_info: ClipBlockInfo = {
|
|
||||||
type: 'image',
|
|
||||||
properties: {
|
|
||||||
image: {
|
|
||||||
value: result.id,
|
|
||||||
name: file.name,
|
|
||||||
size: file.size,
|
|
||||||
type: file.type,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
children: [] as ClipBlockInfo[],
|
|
||||||
};
|
|
||||||
this.insert_blocks([block_info]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private get_image_file(clipboardData: any) {
|
|
||||||
const files = clipboardData.files;
|
|
||||||
if (files && files[0] && files[0].type.indexOf('image') > -1) {
|
|
||||||
return files[0];
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
private excape_html(data: any, onlySpace?: any) {
|
|
||||||
if (!onlySpace) {
|
|
||||||
// TODO:
|
|
||||||
// data = string.htmlEscape(data);
|
|
||||||
// data = data.replace(/\n/g, '<br>');
|
|
||||||
}
|
|
||||||
|
|
||||||
// data = data.replace(/ /g, ' ');
|
|
||||||
// data = data.replace(/\t/g, ' ');
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getOptimalClip(clipboardData: any) {
|
|
||||||
const mimeTypeArr = BrowserClipboard.optimal_mime_type;
|
|
||||||
|
|
||||||
for (let i = 0; i < mimeTypeArr.length; i++) {
|
|
||||||
const data =
|
|
||||||
clipboardData[mimeTypeArr[i]] ||
|
|
||||||
clipboardData.getData(mimeTypeArr[i]);
|
|
||||||
|
|
||||||
if (data) {
|
|
||||||
return {
|
|
||||||
type: mimeTypeArr[i],
|
|
||||||
data: data,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
private is_pure_file_in_clipboard(clipboardData: DataTransfer) {
|
|
||||||
const types = clipboardData.types;
|
|
||||||
|
|
||||||
const res =
|
|
||||||
(types.length === 1 && types[0] === 'Files') ||
|
|
||||||
(types.length === 2 &&
|
|
||||||
(types.includes('text/plain') || types.includes('text/html')) &&
|
|
||||||
types.includes('Files'));
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async fire_paste_edit_action(clipboardData: any) {
|
|
||||||
const clip_info: InnerClipInfo = JSON.parse(clipboardData);
|
|
||||||
clip_info && this.insert_blocks(clip_info.data, clip_info.select);
|
|
||||||
}
|
|
||||||
|
|
||||||
private can_edit_text(type: BlockFlavorKeys) {
|
|
||||||
return (
|
|
||||||
type === Protocol.Block.Type.page ||
|
|
||||||
type === Protocol.Block.Type.text ||
|
|
||||||
type === Protocol.Block.Type.heading1 ||
|
|
||||||
type === Protocol.Block.Type.heading2 ||
|
|
||||||
type === Protocol.Block.Type.heading3 ||
|
|
||||||
type === Protocol.Block.Type.quote ||
|
|
||||||
type === Protocol.Block.Type.todo ||
|
|
||||||
type === Protocol.Block.Type.code ||
|
|
||||||
type === Protocol.Block.Type.callout ||
|
|
||||||
type === Protocol.Block.Type.numbered ||
|
|
||||||
type === Protocol.Block.Type.bullet
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: cursor positioning problem
|
|
||||||
private async insert_blocks(blocks: ClipBlockInfo[], select?: SelectInfo) {
|
|
||||||
if (blocks.length === 0) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cur_select_info =
|
this._dispatchClipboardEvent(ClipboardAction.CUT, e as ClipboardEvent);
|
||||||
await this.editor.selectionManager.getSelectInfo();
|
|
||||||
if (cur_select_info.blocks.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let begin_index = 0;
|
|
||||||
const cur_node_id =
|
|
||||||
cur_select_info.blocks[cur_select_info.blocks.length - 1].blockId;
|
|
||||||
let cur_block = await this.editor.getBlockById(cur_node_id);
|
|
||||||
const block_view = this.editor.getView(cur_block.type);
|
|
||||||
if (
|
|
||||||
cur_select_info.type === 'Range' &&
|
|
||||||
cur_block.type === 'text' &&
|
|
||||||
block_view.isEmpty(cur_block)
|
|
||||||
) {
|
|
||||||
cur_block.setType(blocks[0].type);
|
|
||||||
cur_block.setProperties(blocks[0].properties);
|
|
||||||
this.paste_children(cur_block, blocks[0].children);
|
|
||||||
begin_index = 1;
|
|
||||||
} else if (
|
|
||||||
select?.type === 'Range' &&
|
|
||||||
cur_select_info.type === 'Range' &&
|
|
||||||
this.can_edit_text(cur_block.type) &&
|
|
||||||
this.can_edit_text(blocks[0].type)
|
|
||||||
) {
|
|
||||||
if (
|
|
||||||
cur_select_info.blocks.length > 0 &&
|
|
||||||
cur_select_info.blocks[0].startInfo
|
|
||||||
) {
|
|
||||||
const start_info = cur_select_info.blocks[0].startInfo;
|
|
||||||
const end_info = cur_select_info.blocks[0].endInfo;
|
|
||||||
const cur_text_value = cur_block.getProperty('text').value;
|
|
||||||
const pre_cur_text_value = cur_text_value.slice(
|
|
||||||
0,
|
|
||||||
start_info.arrayIndex
|
|
||||||
);
|
|
||||||
const last_cur_text_value = cur_text_value.slice(
|
|
||||||
end_info.arrayIndex + 1
|
|
||||||
);
|
|
||||||
const pre_text = cur_text_value[
|
|
||||||
start_info.arrayIndex
|
|
||||||
].text.substring(0, start_info.offset);
|
|
||||||
const last_text = cur_text_value[
|
|
||||||
end_info.arrayIndex
|
|
||||||
].text.substring(end_info.offset);
|
|
||||||
|
|
||||||
let last_block: ClipBlockInfo = blocks[blocks.length - 1];
|
|
||||||
if (!this.can_edit_text(last_block.type)) {
|
|
||||||
last_block = { type: 'text', children: [] };
|
|
||||||
blocks.push(last_block);
|
|
||||||
}
|
|
||||||
const last_values = last_block.properties?.text?.value;
|
|
||||||
last_text && last_values.push({ text: last_text });
|
|
||||||
last_values.push(...last_cur_text_value);
|
|
||||||
last_block.properties = {
|
|
||||||
text: { value: last_values },
|
|
||||||
};
|
|
||||||
|
|
||||||
const insert_info = blocks[0].properties.text;
|
|
||||||
pre_text && pre_cur_text_value.push({ text: pre_text });
|
|
||||||
pre_cur_text_value.push(...insert_info.value);
|
|
||||||
this.editor.blockHelper.setBlockBlur(cur_node_id);
|
|
||||||
setTimeout(async () => {
|
|
||||||
const cur_block = await this.editor.getBlockById(
|
|
||||||
cur_node_id
|
|
||||||
);
|
|
||||||
cur_block.setProperties({
|
|
||||||
text: { value: pre_cur_text_value },
|
|
||||||
});
|
|
||||||
this.paste_children(cur_block, blocks[0].children);
|
|
||||||
}, 0);
|
|
||||||
begin_index = 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = begin_index; i < blocks.length; i++) {
|
|
||||||
const next_block = await this.editor.createBlock(blocks[i].type);
|
|
||||||
next_block.setProperties(blocks[i].properties);
|
|
||||||
if (cur_block.type === 'page') {
|
|
||||||
cur_block.prepend(next_block);
|
|
||||||
} else {
|
|
||||||
cur_block.after(next_block);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.paste_children(next_block, blocks[i].children);
|
|
||||||
cur_block = next_block;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async paste_children(parent: AsyncBlock, children: any[]) {
|
private _preCopyCut(action: ClipboardAction, e: ClipboardEvent) {
|
||||||
for (let i = 0; i < children.length; i++) {
|
|
||||||
const next_block = await this.editor.createBlock(children[i].type);
|
|
||||||
next_block.setProperties(children[i].properties);
|
|
||||||
parent.append(next_block);
|
|
||||||
await this.paste_children(next_block, children[i].children);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private pre_copy_cut(action: ClipboardAction, e: ClipboardEvent) {
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case ClipboardAction.COPY:
|
case ClipboardAction.COPY:
|
||||||
this.hooks.beforeCopy(e);
|
this._hooks.beforeCopy(e);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ClipboardAction.CUT:
|
case ClipboardAction.CUT:
|
||||||
this.hooks.beforeCut(e);
|
this._hooks.beforeCut(e);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private dispatch_clipboard_event(
|
private _dispatchClipboardEvent(
|
||||||
action: ClipboardAction,
|
action: ClipboardAction,
|
||||||
e: ClipboardEvent
|
e: ClipboardEvent
|
||||||
) {
|
) {
|
||||||
this.pre_copy_cut(action, e);
|
this._preCopyCut(action, e);
|
||||||
}
|
}
|
||||||
|
|
||||||
dispose() {
|
dispose() {
|
||||||
document.removeEventListener(ClipboardAction.COPY, this.handle_copy);
|
document.removeEventListener(ClipboardAction.COPY, this._handleCopy);
|
||||||
document.removeEventListener(ClipboardAction.CUT, this.handle_cut);
|
document.removeEventListener(ClipboardAction.CUT, this._handleCut);
|
||||||
document.removeEventListener(ClipboardAction.PASTE, this.handle_paste);
|
document.removeEventListener(
|
||||||
this.event_target.removeEventListener(
|
|
||||||
ClipboardAction.COPY,
|
|
||||||
this.handle_copy
|
|
||||||
);
|
|
||||||
this.event_target.removeEventListener(
|
|
||||||
ClipboardAction.CUT,
|
|
||||||
this.handle_cut
|
|
||||||
);
|
|
||||||
this.event_target.removeEventListener(
|
|
||||||
ClipboardAction.PASTE,
|
ClipboardAction.PASTE,
|
||||||
this.handle_paste
|
this._paste.handlePaste
|
||||||
);
|
);
|
||||||
this.clipboard_parse.dispose();
|
this._eventTarget.removeEventListener(
|
||||||
this.clipboard_parse = null;
|
ClipboardAction.COPY,
|
||||||
this.event_target = null;
|
this._handleCopy
|
||||||
this.hooks = null;
|
);
|
||||||
this.editor = null;
|
this._eventTarget.removeEventListener(
|
||||||
|
ClipboardAction.CUT,
|
||||||
|
this._handleCut
|
||||||
|
);
|
||||||
|
this._eventTarget.removeEventListener(
|
||||||
|
ClipboardAction.PASTE,
|
||||||
|
this._paste.handlePaste
|
||||||
|
);
|
||||||
|
this._clipboardParse.dispose();
|
||||||
|
this._clipboardParse = null;
|
||||||
|
this._eventTarget = null;
|
||||||
|
this._hooks = null;
|
||||||
|
this._editor = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -115,10 +115,8 @@ export default class ClipboardParse {
|
|||||||
const block_utils = this.editor.getView(
|
const block_utils = this.editor.getView(
|
||||||
ClipboardParse.block_types[i]
|
ClipboardParse.block_types[i]
|
||||||
);
|
);
|
||||||
const blocks =
|
const blocks = block_utils?.html2block?.(el, this.parse_dom);
|
||||||
block_utils &&
|
|
||||||
block_utils.html2block &&
|
|
||||||
block_utils.html2block(el, this.parse_dom);
|
|
||||||
if (blocks) {
|
if (blocks) {
|
||||||
return blocks;
|
return blocks;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ class ClipboardPopulator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: is not compatible with safari
|
// TODO: is not compatible with safari
|
||||||
const success = this.copy_to_cliboard_from_pc(clips);
|
const success = this._copyToClipboardFromPc(clips);
|
||||||
if (!success) {
|
if (!success) {
|
||||||
// This way, not compatible with firefox
|
// This way, not compatible with firefox
|
||||||
const clipboardData = e.clipboardData;
|
const clipboardData = e.clipboardData;
|
||||||
@@ -65,7 +65,7 @@ class ClipboardPopulator {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private copy_to_cliboard_from_pc(clips: any[]) {
|
private _copyToClipboardFromPc(clips: any[]) {
|
||||||
let success = false;
|
let success = false;
|
||||||
const tempElem = document.createElement('textarea');
|
const tempElem = document.createElement('textarea');
|
||||||
tempElem.value = 'temp';
|
tempElem.value = 'temp';
|
||||||
@@ -96,56 +96,55 @@ class ClipboardPopulator {
|
|||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async get_clip_block_info(selBlock: SelectBlock) {
|
private async _getClipBlockInfo(selBlock: SelectBlock) {
|
||||||
const block = await this._editor.getBlockById(selBlock.blockId);
|
const block = await this._editor.getBlockById(selBlock.blockId);
|
||||||
const block_view = this._editor.getView(block.type);
|
const blockView = this._editor.getView(block.type);
|
||||||
assert(block_view);
|
assert(blockView);
|
||||||
const block_info: ClipBlockInfo = {
|
const blockInfo: ClipBlockInfo = {
|
||||||
type: block.type,
|
type: block.type,
|
||||||
properties: block_view.getSelProperties(block, selBlock),
|
properties: blockView.getSelProperties(block, selBlock),
|
||||||
children: [] as any[],
|
children: [] as any[],
|
||||||
};
|
};
|
||||||
|
|
||||||
for (let i = 0; i < selBlock.children.length; i++) {
|
for (let i = 0; i < selBlock.children.length; i++) {
|
||||||
const child_info = await this.get_clip_block_info(
|
const childInfo = await this._getClipBlockInfo(
|
||||||
selBlock.children[i]
|
selBlock.children[i]
|
||||||
);
|
);
|
||||||
block_info.children.push(child_info);
|
blockInfo.children.push(childInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
return block_info;
|
return blockInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async get_inner_clip(): Promise<InnerClipInfo> {
|
private async _getInnerClip(): Promise<InnerClipInfo> {
|
||||||
const clips: ClipBlockInfo[] = [];
|
const clips: ClipBlockInfo[] = [];
|
||||||
const select_info: SelectInfo =
|
const selectInfo: SelectInfo =
|
||||||
await this._selectionManager.getSelectInfo();
|
await this._selectionManager.getSelectInfo();
|
||||||
for (let i = 0; i < select_info.blocks.length; i++) {
|
for (let i = 0; i < selectInfo.blocks.length; i++) {
|
||||||
const sel_block = select_info.blocks[i];
|
const selBlock = selectInfo.blocks[i];
|
||||||
const clip_block_info = await this.get_clip_block_info(sel_block);
|
const clipBlockInfo = await this._getClipBlockInfo(selBlock);
|
||||||
clips.push(clip_block_info);
|
clips.push(clipBlockInfo);
|
||||||
}
|
}
|
||||||
const clipInfo: InnerClipInfo = {
|
return {
|
||||||
select: select_info,
|
select: selectInfo,
|
||||||
data: clips,
|
data: clips,
|
||||||
};
|
};
|
||||||
return clipInfo;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getClips() {
|
async getClips() {
|
||||||
const clips: any[] = [];
|
const clips: any[] = [];
|
||||||
|
|
||||||
const inner_clip = await this.get_inner_clip();
|
const innerClip = await this._getInnerClip();
|
||||||
clips.push(
|
clips.push(
|
||||||
new Clip(
|
new Clip(
|
||||||
OFFICE_CLIPBOARD_MIMETYPE.DOCS_DOCUMENT_SLICE_CLIP_WRAPPED,
|
OFFICE_CLIPBOARD_MIMETYPE.DOCS_DOCUMENT_SLICE_CLIP_WRAPPED,
|
||||||
JSON.stringify(inner_clip)
|
JSON.stringify(innerClip)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
const html_clip = await this._clipboardParse.generateHtml();
|
const htmlClip = await this._clipboardParse.generateHtml();
|
||||||
html_clip &&
|
htmlClip &&
|
||||||
clips.push(new Clip(OFFICE_CLIPBOARD_MIMETYPE.HTML, html_clip));
|
clips.push(new Clip(OFFICE_CLIPBOARD_MIMETYPE.HTML, htmlClip));
|
||||||
|
|
||||||
return clips;
|
return clips;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,455 @@
|
|||||||
|
/* eslint-disable max-lines */
|
||||||
|
import {
|
||||||
|
OFFICE_CLIPBOARD_MIMETYPE,
|
||||||
|
InnerClipInfo,
|
||||||
|
ClipBlockInfo,
|
||||||
|
} from './types';
|
||||||
|
import { Editor } from '../editor';
|
||||||
|
import { AsyncBlock } from '../block';
|
||||||
|
import ClipboardParse from './clipboard-parse';
|
||||||
|
import { SelectInfo } from '../selection';
|
||||||
|
import {
|
||||||
|
Protocol,
|
||||||
|
BlockFlavorKeys,
|
||||||
|
services,
|
||||||
|
} from '@toeverything/datasource/db-service';
|
||||||
|
import { MarkdownParser } from './markdown-parse';
|
||||||
|
import { shouldHandlerContinue } from './utils';
|
||||||
|
const SUPPORT_MARKDOWN_PASTE = true;
|
||||||
|
|
||||||
|
type TextValueItem = {
|
||||||
|
text: string;
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class Paste {
|
||||||
|
private _editor: Editor;
|
||||||
|
private _markdownParse: MarkdownParser;
|
||||||
|
private _clipboardParse: ClipboardParse;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
editor: Editor,
|
||||||
|
clipboardParse: ClipboardParse,
|
||||||
|
markdownParse: MarkdownParser
|
||||||
|
) {
|
||||||
|
this._markdownParse = markdownParse;
|
||||||
|
this._clipboardParse = clipboardParse;
|
||||||
|
this._editor = editor;
|
||||||
|
this.handlePaste = this.handlePaste.bind(this);
|
||||||
|
}
|
||||||
|
private static _optimalMimeType: string[] = [
|
||||||
|
OFFICE_CLIPBOARD_MIMETYPE.DOCS_DOCUMENT_SLICE_CLIP_WRAPPED,
|
||||||
|
OFFICE_CLIPBOARD_MIMETYPE.HTML,
|
||||||
|
OFFICE_CLIPBOARD_MIMETYPE.TEXT,
|
||||||
|
];
|
||||||
|
public handlePaste(e: Event) {
|
||||||
|
if (!shouldHandlerContinue(e, this._editor)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
const clipboardData = (e as ClipboardEvent).clipboardData;
|
||||||
|
|
||||||
|
const isPureFile = Paste._isPureFileInClipboard(clipboardData);
|
||||||
|
if (isPureFile) {
|
||||||
|
this._pasteFile(clipboardData);
|
||||||
|
} else {
|
||||||
|
this._pasteContent(clipboardData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public getOptimalClip(clipboardData: any) {
|
||||||
|
const mimeTypeArr = Paste._optimalMimeType;
|
||||||
|
|
||||||
|
for (let i = 0; i < mimeTypeArr.length; i++) {
|
||||||
|
const data =
|
||||||
|
clipboardData[mimeTypeArr[i]] ||
|
||||||
|
clipboardData.getData(mimeTypeArr[i]);
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
return {
|
||||||
|
type: mimeTypeArr[i],
|
||||||
|
data: data,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private _pasteContent(clipboardData: any) {
|
||||||
|
const originClip: { data: any; type: any } = this.getOptimalClip(
|
||||||
|
clipboardData
|
||||||
|
) as { data: any; type: any };
|
||||||
|
|
||||||
|
const originTextClipData = clipboardData.getData(
|
||||||
|
OFFICE_CLIPBOARD_MIMETYPE.TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
let clipData = originClip['data'];
|
||||||
|
|
||||||
|
if (originClip['type'] === OFFICE_CLIPBOARD_MIMETYPE.TEXT) {
|
||||||
|
clipData = Paste._excapeHtml(clipData);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (originClip['type']) {
|
||||||
|
/** Protocol paste */
|
||||||
|
case OFFICE_CLIPBOARD_MIMETYPE.DOCS_DOCUMENT_SLICE_CLIP_WRAPPED:
|
||||||
|
this._firePasteEditAction(clipData);
|
||||||
|
break;
|
||||||
|
case OFFICE_CLIPBOARD_MIMETYPE.HTML:
|
||||||
|
this._pasteHtml(clipData, originTextClipData);
|
||||||
|
break;
|
||||||
|
case OFFICE_CLIPBOARD_MIMETYPE.TEXT:
|
||||||
|
this._pasteText(clipData, originTextClipData);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private async _firePasteEditAction(clipboardData: any) {
|
||||||
|
const clipInfo: InnerClipInfo = JSON.parse(clipboardData);
|
||||||
|
clipInfo && this._insertBlocks(clipInfo.data, clipInfo.select);
|
||||||
|
}
|
||||||
|
private async _pasteFile(clipboardData: any) {
|
||||||
|
const file = Paste._getImageFile(clipboardData);
|
||||||
|
if (file) {
|
||||||
|
const result = await services.api.file.create({
|
||||||
|
workspace: this._editor.workspace,
|
||||||
|
file: file,
|
||||||
|
});
|
||||||
|
const blockInfo: ClipBlockInfo = {
|
||||||
|
type: 'image',
|
||||||
|
properties: {
|
||||||
|
image: {
|
||||||
|
value: result.id,
|
||||||
|
name: file.name,
|
||||||
|
size: file.size,
|
||||||
|
type: file.type,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
children: [] as ClipBlockInfo[],
|
||||||
|
};
|
||||||
|
await this._insertBlocks([blockInfo]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private static _isPureFileInClipboard(clipboardData: DataTransfer) {
|
||||||
|
const types = clipboardData.types;
|
||||||
|
|
||||||
|
return (
|
||||||
|
(types.length === 1 && types[0] === 'Files') ||
|
||||||
|
(types.length === 2 &&
|
||||||
|
(types.includes('text/plain') || types.includes('text/html')) &&
|
||||||
|
types.includes('Files'))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static _isTextEditBlock(type: BlockFlavorKeys) {
|
||||||
|
return (
|
||||||
|
type === Protocol.Block.Type.page ||
|
||||||
|
type === Protocol.Block.Type.text ||
|
||||||
|
type === Protocol.Block.Type.heading1 ||
|
||||||
|
type === Protocol.Block.Type.heading2 ||
|
||||||
|
type === Protocol.Block.Type.heading3 ||
|
||||||
|
type === Protocol.Block.Type.quote ||
|
||||||
|
type === Protocol.Block.Type.todo ||
|
||||||
|
type === Protocol.Block.Type.code ||
|
||||||
|
type === Protocol.Block.Type.callout ||
|
||||||
|
type === Protocol.Block.Type.numbered ||
|
||||||
|
type === Protocol.Block.Type.bullet
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _insertBlocks(
|
||||||
|
blocks: ClipBlockInfo[],
|
||||||
|
pasteSelect?: SelectInfo
|
||||||
|
) {
|
||||||
|
if (blocks.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const currentSelectInfo =
|
||||||
|
await this._editor.selectionManager.getSelectInfo();
|
||||||
|
|
||||||
|
// When the selection is in one of the blocks, select?.type === 'Range'
|
||||||
|
// Currently the selection does not support cross-blocking, so this case is not considered
|
||||||
|
if (currentSelectInfo.type === 'Range') {
|
||||||
|
// 当 currentSelectInfo.type === 'Range' 时,光标选中的block必然只有一个
|
||||||
|
const selectedBlock = await this._editor.getBlockById(
|
||||||
|
currentSelectInfo.blocks[0].blockId
|
||||||
|
);
|
||||||
|
const isSelectedBlockEdit = Paste._isTextEditBlock(
|
||||||
|
selectedBlock.type
|
||||||
|
);
|
||||||
|
if (isSelectedBlockEdit) {
|
||||||
|
const shouldSplitBlock =
|
||||||
|
blocks.length > 1 ||
|
||||||
|
!Paste._isTextEditBlock(blocks[0].type);
|
||||||
|
const pureText = !shouldSplitBlock
|
||||||
|
? blocks[0].properties.text.value
|
||||||
|
: [{ text: '' }];
|
||||||
|
const { startInfo, endInfo } = currentSelectInfo.blocks[0];
|
||||||
|
|
||||||
|
// Text content of the selected current editable block
|
||||||
|
const currentTextValue =
|
||||||
|
selectedBlock.getProperty('text').value;
|
||||||
|
// When the cursor selection spans different styles of text
|
||||||
|
if (startInfo?.arrayIndex !== endInfo?.arrayIndex) {
|
||||||
|
if (shouldSplitBlock) {
|
||||||
|
// TODO: split block maybe should use slate method to support, like "this._editor.blockHelper.insertNodes"
|
||||||
|
const newTextValue = currentTextValue.reduce(
|
||||||
|
(
|
||||||
|
newTextValue: TextValueItem[],
|
||||||
|
textStore: TextValueItem,
|
||||||
|
i: number
|
||||||
|
) => {
|
||||||
|
if (i < startInfo?.arrayIndex) {
|
||||||
|
newTextValue.push(textStore);
|
||||||
|
}
|
||||||
|
const { text, ...props } = textStore;
|
||||||
|
|
||||||
|
if (i === startInfo?.arrayIndex) {
|
||||||
|
newTextValue.push({
|
||||||
|
text: text.slice(0, startInfo?.offset),
|
||||||
|
...props,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return newTextValue;
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
const nextTextValue = currentTextValue.reduce(
|
||||||
|
(
|
||||||
|
newTextValue: TextValueItem[],
|
||||||
|
textStore: TextValueItem,
|
||||||
|
i: number
|
||||||
|
) => {
|
||||||
|
if (i > endInfo?.arrayIndex) {
|
||||||
|
newTextValue.push(textStore);
|
||||||
|
}
|
||||||
|
const { text, ...props } = textStore;
|
||||||
|
|
||||||
|
if (i === endInfo?.arrayIndex) {
|
||||||
|
newTextValue.push({
|
||||||
|
text: text.slice(endInfo?.offset),
|
||||||
|
...props,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return newTextValue;
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
selectedBlock.setProperties({
|
||||||
|
text: {
|
||||||
|
value: newTextValue,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const pasteBlocks = await this._createBlocks(blocks);
|
||||||
|
pasteBlocks.forEach(block => {
|
||||||
|
selectedBlock.after(block);
|
||||||
|
});
|
||||||
|
const nextBlock = await this._editor.createBlock(
|
||||||
|
selectedBlock?.type
|
||||||
|
);
|
||||||
|
nextBlock.setProperties({
|
||||||
|
text: {
|
||||||
|
value: nextTextValue,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
pasteBlocks[pasteBlocks.length - 1].after(nextBlock);
|
||||||
|
|
||||||
|
this._setEndSelectToBlock(
|
||||||
|
pasteBlocks[pasteBlocks.length - 1].id
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this._editor.blockHelper.insertNodes(
|
||||||
|
selectedBlock.id,
|
||||||
|
pureText,
|
||||||
|
{ select: true }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// When the cursor selection does not span different styles of text
|
||||||
|
if (startInfo?.arrayIndex === endInfo?.arrayIndex) {
|
||||||
|
if (shouldSplitBlock) {
|
||||||
|
// TODO: split block maybe should use slate method to support, like "this._editor.blockHelper.insertNodes"
|
||||||
|
const newTextValue = currentTextValue.reduce(
|
||||||
|
(
|
||||||
|
newTextValue: TextValueItem[],
|
||||||
|
textStore: TextValueItem,
|
||||||
|
i: number
|
||||||
|
) => {
|
||||||
|
if (i < startInfo?.arrayIndex) {
|
||||||
|
newTextValue.push(textStore);
|
||||||
|
}
|
||||||
|
const { text, ...props } = textStore;
|
||||||
|
|
||||||
|
if (i === startInfo?.arrayIndex) {
|
||||||
|
newTextValue.push({
|
||||||
|
text: `${text.slice(
|
||||||
|
0,
|
||||||
|
startInfo?.offset
|
||||||
|
)}`,
|
||||||
|
...props,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return newTextValue;
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const nextTextValue = currentTextValue.reduce(
|
||||||
|
(
|
||||||
|
nextTextValue: TextValueItem[],
|
||||||
|
textStore: TextValueItem,
|
||||||
|
i: number
|
||||||
|
) => {
|
||||||
|
if (i > endInfo?.arrayIndex) {
|
||||||
|
nextTextValue.push(textStore);
|
||||||
|
}
|
||||||
|
const { text, ...props } = textStore;
|
||||||
|
|
||||||
|
if (i === endInfo?.arrayIndex) {
|
||||||
|
nextTextValue.push({
|
||||||
|
text: `${text.slice(endInfo?.offset)}`,
|
||||||
|
...props,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return nextTextValue;
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
selectedBlock.setProperties({
|
||||||
|
text: {
|
||||||
|
value: newTextValue,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const pasteBlocks = await this._createBlocks(blocks);
|
||||||
|
pasteBlocks.forEach((block: AsyncBlock) => {
|
||||||
|
selectedBlock.after(block);
|
||||||
|
});
|
||||||
|
const nextBlock = await this._editor.createBlock(
|
||||||
|
selectedBlock?.type
|
||||||
|
);
|
||||||
|
nextBlock.setProperties({
|
||||||
|
text: {
|
||||||
|
value: nextTextValue,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
pasteBlocks[pasteBlocks.length - 1].after(nextBlock);
|
||||||
|
|
||||||
|
this._setEndSelectToBlock(
|
||||||
|
pasteBlocks[pasteBlocks.length - 1].id
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this._editor.blockHelper.insertNodes(
|
||||||
|
selectedBlock.id,
|
||||||
|
pureText,
|
||||||
|
{ select: true }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const pasteBlocks = await this._createBlocks(blocks);
|
||||||
|
pasteBlocks.forEach(block => {
|
||||||
|
selectedBlock.after(block);
|
||||||
|
});
|
||||||
|
this._setEndSelectToBlock(
|
||||||
|
pasteBlocks[pasteBlocks.length - 1].id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentSelectInfo.type === 'Block') {
|
||||||
|
const selectedBlock = await this._editor.getBlockById(
|
||||||
|
currentSelectInfo.blocks[currentSelectInfo.blocks.length - 1]
|
||||||
|
.blockId
|
||||||
|
);
|
||||||
|
const pasteBlocks = await this._createBlocks(blocks);
|
||||||
|
|
||||||
|
let groupBlock: AsyncBlock;
|
||||||
|
if (
|
||||||
|
selectedBlock?.type === 'group' ||
|
||||||
|
selectedBlock?.type === 'page'
|
||||||
|
) {
|
||||||
|
groupBlock = await this._editor.createBlock('group');
|
||||||
|
pasteBlocks.forEach(block => {
|
||||||
|
groupBlock.append(block);
|
||||||
|
});
|
||||||
|
await selectedBlock.after(groupBlock);
|
||||||
|
} else {
|
||||||
|
pasteBlocks.forEach(block => {
|
||||||
|
selectedBlock.after(block);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this._setEndSelectToBlock(pasteBlocks[pasteBlocks.length - 1].id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _setEndSelectToBlock(blockId: string) {
|
||||||
|
setTimeout(() => {
|
||||||
|
this._editor.selectionManager.activeNodeByNodeId(blockId, 'end');
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _createBlocks(blocks: ClipBlockInfo[], parentId?: string) {
|
||||||
|
return Promise.all(
|
||||||
|
blocks.map(async clipBlockInfo => {
|
||||||
|
const block = await this._editor.createBlock(
|
||||||
|
clipBlockInfo.type
|
||||||
|
);
|
||||||
|
block?.setProperties(clipBlockInfo.properties);
|
||||||
|
await this._createBlocks(clipBlockInfo.children, block?.id);
|
||||||
|
return block;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _pasteHtml(clipData: any, originTextClipData: any) {
|
||||||
|
if (SUPPORT_MARKDOWN_PASTE) {
|
||||||
|
const hasMarkdown =
|
||||||
|
this._markdownParse.checkIfTextContainsMd(originTextClipData);
|
||||||
|
if (hasMarkdown) {
|
||||||
|
try {
|
||||||
|
const convertedDataObj =
|
||||||
|
this._markdownParse.md2Html(originTextClipData);
|
||||||
|
if (convertedDataObj.isConverted) {
|
||||||
|
clipData = convertedDataObj.text;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
clipData = originTextClipData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const blocks = this._clipboardParse.html2blocks(clipData);
|
||||||
|
|
||||||
|
await this._insertBlocks(blocks);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _pasteText(clipData: any, originTextClipData: any) {
|
||||||
|
const blocks = this._clipboardParse.text2blocks(clipData);
|
||||||
|
await this._insertBlocks(blocks);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static _getImageFile(clipboardData: any) {
|
||||||
|
const files = clipboardData.files;
|
||||||
|
if (files && files[0] && files[0].type.indexOf('image') > -1) {
|
||||||
|
return files[0];
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static _excapeHtml(data: any, onlySpace?: any) {
|
||||||
|
if (!onlySpace) {
|
||||||
|
// TODO:
|
||||||
|
// data = string.htmlEscape(data);
|
||||||
|
// data = data.replace(/\n/g, '<br>');
|
||||||
|
}
|
||||||
|
|
||||||
|
// data = data.replace(/ /g, ' ');
|
||||||
|
// data = data.replace(/\t/g, ' ');
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { Editor } from '../editor';
|
||||||
|
|
||||||
|
export const shouldHandlerContinue = (event: Event, editor: Editor) => {
|
||||||
|
const filterNodes = ['INPUT', 'SELECT', 'TEXTAREA'];
|
||||||
|
|
||||||
|
if (event.defaultPrevented) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (filterNodes.includes((event.target as HTMLElement)?.tagName)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return editor.selectionManager.currentSelectInfo.type !== 'None';
|
||||||
|
};
|
||||||
@@ -30,6 +30,7 @@ import {
|
|||||||
} from './types';
|
} from './types';
|
||||||
import { isLikeBlockListIds } from './utils';
|
import { isLikeBlockListIds } from './utils';
|
||||||
import { Protocol } from '@toeverything/datasource/db-service';
|
import { Protocol } from '@toeverything/datasource/db-service';
|
||||||
|
import { Editor } from 'slate';
|
||||||
// IMP: maybe merge active and select into single function
|
// IMP: maybe merge active and select into single function
|
||||||
|
|
||||||
export type SelectionInfo = InstanceType<
|
export type SelectionInfo = InstanceType<
|
||||||
@@ -1047,4 +1048,25 @@ export class SelectionManager implements VirgoSelection {
|
|||||||
this._windowSelectionChangeHandler
|
this._windowSelectionChangeHandler
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* move active selection to the new position
|
||||||
|
* @param {number} index
|
||||||
|
* @param {string} blockId
|
||||||
|
* @memberof SelectionManager
|
||||||
|
*/
|
||||||
|
public async moveCursor(
|
||||||
|
nowRange: any,
|
||||||
|
index: number,
|
||||||
|
blockId: string
|
||||||
|
): Promise<void> {
|
||||||
|
let preRang = document.createRange();
|
||||||
|
preRang.setStart(nowRange.startContainer, index);
|
||||||
|
preRang.setEnd(nowRange.endContainer, index);
|
||||||
|
let prePosition = preRang.getClientRects().item(0);
|
||||||
|
this.activeNodeByNodeId(
|
||||||
|
blockId,
|
||||||
|
new Point(prePosition.left, prePosition.bottom)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user