Merge pull request #200 from toeverything/fix/clipboard

Fix/clipboard
This commit is contained in:
Qi
2022-08-12 14:43:26 +08:00
committed by GitHub
9 changed files with 721 additions and 445 deletions

View File

@@ -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() {
return {
anchor: this.getStart(),

View File

@@ -45,13 +45,13 @@ const TextBlockContainer = styled(Text)(({ theme }) => ({
}));
const findSlice = (arr: string[], p: string, q: string) => {
let should_include = false;
let shouldInclude = false;
return arr.filter(block => {
if (block === p || block === q) {
should_include = !should_include;
shouldInclude = !shouldInclude;
return true;
} else {
return should_include;
return shouldInclude;
}
});
};
@@ -115,11 +115,8 @@ export const TextManage = forwardRef<ExtendedTextUtils, CreateTextView>(
(ref as MutableRefObject<ExtendedTextUtils>) || defaultRef;
const properties = block.getProperties();
// const [is_select, set_is_select] = useState<boolean>();
// useOnSelect(block.id, (is_select: boolean) => {
// set_is_select(is_select);
// });
const on_text_view_set_selection = (selection: Range | Point) => {
const onTextViewSetSelection = (selection: Range | Point) => {
if (selection instanceof Point) {
//do some thing
} else {
@@ -128,18 +125,18 @@ export const TextManage = forwardRef<ExtendedTextUtils, CreateTextView>(
};
// block = await editor.commands.blockCommands.createNextBlock(block.id,)
const on_text_view_active = useCallback(
const onTextViewActive = useCallback(
(point: CursorTypes) => {
// TODO code to be optimized
if (textRef.current) {
const end_selection = textRef.current.getEndSelection();
const start_selection = textRef.current.getStartSelection();
const endSelection = textRef.current.getEndSelection();
const startSelection = textRef.current.getStartSelection();
if (point === 'start') {
textRef.current.setSelection(start_selection);
textRef.current.setSelection(startSelection);
return;
}
if (point === 'end') {
textRef.current.setSelection(end_selection);
textRef.current.setSelection(endSelection);
return;
}
try {
@@ -154,24 +151,24 @@ export const TextManage = forwardRef<ExtendedTextUtils, CreateTextView>(
} else {
blockTop = blockDomStyle.top + 5;
}
const end_position = ReactEditor.toDOMRange(
const endPosition = ReactEditor.toDOMRange(
textRef.current.editor,
end_selection
endSelection
)
.getClientRects()
.item(0);
const start_position = ReactEditor.toDOMRange(
const startPosition = ReactEditor.toDOMRange(
textRef.current.editor,
start_selection
startSelection
)
.getClientRects()
.item(0);
if (end_position.left <= point.x) {
textRef.current.setSelection(end_selection);
if (endPosition.left <= point.x) {
textRef.current.setSelection(endSelection);
return;
}
if (start_position.left >= point.x) {
textRef.current.setSelection(start_selection);
if (startPosition.left >= point.x) {
textRef.current.setSelection(startSelection);
return;
}
let range: globalThis.Range;
@@ -189,7 +186,7 @@ export const TextManage = forwardRef<ExtendedTextUtils, CreateTextView>(
range = document.createRange();
range.setStart(caret.offsetNode, caret.offset);
}
const slate_rang = ReactEditor.toSlateRange(
const slateRang = ReactEditor.toSlateRange(
textRef.current.editor,
range,
{
@@ -197,19 +194,19 @@ export const TextManage = forwardRef<ExtendedTextUtils, CreateTextView>(
suppressThrow: true,
}
);
textRef.current.setSelection(slate_rang);
textRef.current.setSelection(slateRang);
}
} catch (e) {
console.log('e: ', e);
textRef.current.setSelection(end_selection);
textRef.current.setSelection(endSelection);
}
}
},
[textRef]
);
useOnSelectActive(block.id, on_text_view_active);
useOnSelectSetSelection<'Range'>(block.id, on_text_view_set_selection);
useOnSelectActive(block.id, onTextViewActive);
useOnSelectSetSelection<'Range'>(block.id, onTextViewSetSelection);
useEffect(() => {
if (textRef.current) {
@@ -235,17 +232,17 @@ export const TextManage = forwardRef<ExtendedTextUtils, CreateTextView>(
(block.id === lastSelectNodeId && type === 'Range') ||
(type === 'Range' && info)
) {
on_text_view_active('end');
onTextViewActive('end');
} else {
on_text_view_active('start');
onTextViewActive('start');
}
}
} catch (e) {
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,
textStyle
) => {
@@ -266,39 +263,34 @@ export const TextManage = forwardRef<ExtendedTextUtils, CreateTextView>(
});
}
};
const get_now_and_pre_rang_position = () => {
window.getSelection().getRangeAt(0);
// const now_range =
// editor.selectionManager.currentSelectInfo?.browserSelection.getRangeAt(
// 0
// );
const now_range = window.getSelection().getRangeAt(0);
let pre_position = null;
const now_position = now_range.getClientRects().item(0);
const getNowAndPreRangPosition = () => {
const nowRange = window.getSelection().getRangeAt(0);
let prePosition = null;
const nowPosition = nowRange.getClientRects().item(0);
try {
if (now_range.startOffset !== 0) {
const pre_rang = document.createRange();
pre_rang.setStart(
now_range.startContainer,
now_range.startOffset + 1
if (nowRange.startOffset !== 0) {
const preRang = document.createRange();
preRang.setStart(
nowRange.startContainer,
nowRange.startOffset + 1
);
pre_rang.setEnd(
now_range.endContainer,
now_range.endOffset + 1
preRang.setEnd(
nowRange.endContainer,
nowRange.endOffset + 1
);
pre_position = pre_rang.getClientRects().item(0);
prePosition = preRang.getClientRects().item(0);
}
} catch (e) {
// console.log(e);
}
return { nowPosition: now_position, prePosition: pre_position };
return { nowPosition: nowPosition, prePosition: prePosition };
};
const onKeyboardUp = (event: React.KeyboardEvent<Element>) => {
// if default event is prevented do noting
// if U want to disable up/down/enter use capture event for preventing
if (!event.isDefaultPrevented()) {
const positions = get_now_and_pre_rang_position();
const positions = getNowAndPreRangPosition();
const prePosition = positions.prePosition;
const nowPosition = positions.nowPosition;
if (prePosition) {
@@ -339,7 +331,7 @@ export const TextManage = forwardRef<ExtendedTextUtils, CreateTextView>(
// editor.selectionManager.activeNextNode(block.id, 'start');
// return;
if (!event.isDefaultPrevented()) {
const positions = get_now_and_pre_rang_position();
const positions = getNowAndPreRangPosition();
const prePosition = positions.prePosition;
const nowPosition = positions.nowPosition;
// Create the last element range of slate_editor
@@ -395,7 +387,7 @@ export const TextManage = forwardRef<ExtendedTextUtils, CreateTextView>(
return false;
}
};
const on_select_all = () => {
const onSelectAll = () => {
const isSelectAll =
textRef.current.isEmpty() || textRef.current.isSelectAll();
if (isSelectAll) {
@@ -405,22 +397,20 @@ export const TextManage = forwardRef<ExtendedTextUtils, CreateTextView>(
return false;
};
const on_undo = () => {
const onUndo = () => {
editor.undo();
};
const on_redo = () => {
const onRedo = () => {
editor.redo();
};
const on_keyboard_esc = () => {
const onKeyboardEsc = () => {
if (editor.selectionManager.getSelectedNodesIds().length === 0) {
const active_node_id =
const activeNodeId =
editor.selectionManager.getActivatedNodeId();
if (active_node_id) {
editor.selectionManager.setSelectedNodesIds([
active_node_id,
]);
if (activeNodeId) {
editor.selectionManager.setSelectedNodesIds([activeNodeId]);
ReactEditor.blur(textRef.current.editor);
}
} 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) {
const activeId = editor.selectionManager.getActivatedNodeId();
if (activeId === block.id) {
@@ -477,16 +467,16 @@ export const TextManage = forwardRef<ExtendedTextUtils, CreateTextView>(
className={`${otherOptions.className}`}
currentValue={properties.text.value}
textStyle={properties.textStyle}
handleChange={on_text_change}
handleChange={onTextChange}
handleUp={onKeyboardUp}
handleDown={onKeyboardDown}
handleLeft={onKeyboardLeft}
handleRight={onKeyboardRight}
handleSelectAll={on_select_all}
handleMouseDown={on_shift_click}
handleUndo={on_undo}
handleRedo={on_redo}
handleEsc={on_keyboard_esc}
handleSelectAll={onSelectAll}
handleMouseDown={onShiftClick}
handleUndo={onUndo}
handleRedo={onRedo}
handleEsc={onKeyboardEsc}
{...otherOptions}
/>
);

View File

@@ -3,7 +3,13 @@ import type {
SlateUtils,
TextAlignOptions,
} 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';
type TextUtilsFunctions =
@@ -28,7 +34,10 @@ type TextUtilsFunctions =
| 'removeSelection'
| 'insertReference'
| 'isCollapsed'
| 'blur';
| 'blur'
| 'setSelection'
| 'insertNodes'
| 'getNodeByPath';
type ExtendedTextUtils = SlateUtils & {
setLinkModalVisible: (visible: boolean) => void;
@@ -193,6 +202,59 @@ export class BlockHelper {
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(
blockId: string,
...restArgs: Parameters<TextUtils['transformPoint']>

View File

@@ -14,403 +14,126 @@ import {
services,
} from '@toeverything/datasource/db-service';
import { MarkdownParser } from './markdown-parse';
import { shouldHandlerContinue } from './utils';
import { Paste } from './paste';
// todo needs to be a switch
const support_markdown_paste = true;
enum ClipboardAction {
COPY = 'copy',
CUT = 'cut',
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[] = [
OFFICE_CLIPBOARD_MIMETYPE.DOCS_DOCUMENT_SLICE_CLIP_WRAPPED,
OFFICE_CLIPBOARD_MIMETYPE.HTML,
OFFICE_CLIPBOARD_MIMETYPE.TEXT,
];
//TODO: need to consider the cursor position after inserting the children
class BrowserClipboard {
private _eventTarget: Element;
private _hooks: HooksRunner;
private _editor: Editor;
private _clipboardParse: ClipboardParse;
private _markdownParse: MarkdownParser;
private _paste: Paste;
constructor(eventTarget: Element, hooks: HooksRunner, editor: Editor) {
this.event_target = eventTarget;
this.hooks = hooks;
this.editor = editor;
this.clipboard_parse = new ClipboardParse(editor);
this.markdown_parse = new MarkdownParser();
this.initialize();
this._eventTarget = eventTarget;
this._hooks = hooks;
this._editor = editor;
this._clipboardParse = new ClipboardParse(editor);
this._markdownParse = new MarkdownParser();
this._paste = new Paste(
editor,
this._clipboardParse,
this._markdownParse
);
this._initialize();
}
public getClipboardParse() {
return this.clipboard_parse;
return this._clipboardParse;
}
private initialize() {
this.handle_copy = this.handle_copy.bind(this);
this.handle_cut = this.handle_cut.bind(this);
this.handle_paste = this.handle_paste.bind(this);
private _initialize() {
this._handleCopy = this._handleCopy.bind(this);
this._handleCut = this._handleCut.bind(this);
document.addEventListener(ClipboardAction.COPY, this.handle_copy);
document.addEventListener(ClipboardAction.CUT, this.handle_cut);
document.addEventListener(ClipboardAction.PASTE, this.handle_paste);
this.event_target.addEventListener(
ClipboardAction.COPY,
this.handle_copy
);
this.event_target.addEventListener(
ClipboardAction.CUT,
this.handle_cut
);
this.event_target.addEventListener(
document.addEventListener(ClipboardAction.COPY, this._handleCopy);
document.addEventListener(ClipboardAction.CUT, this._handleCut);
document.addEventListener(
ClipboardAction.PASTE,
this.handle_paste
this._paste.handlePaste
);
}
private handle_copy(e: Event) {
//@ts-ignore
if (e.defaultPrevented || e.target.nodeName === 'INPUT') {
return;
}
this.dispatch_clipboard_event(
this._eventTarget.addEventListener(
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) {
//@ts-ignore
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') {
private _handleCopy(e: Event) {
if (!shouldHandlerContinue(e, this._editor)) {
return;
}
const clipboardData = (e as ClipboardEvent).clipboardData;
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();
this._dispatchClipboardEvent(ClipboardAction.COPY, e as ClipboardEvent);
}
private paste_content(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 = 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, '&nbsp;');
// data = data.replace(/\t/g, '&nbsp;&nbsp;&nbsp;&nbsp;');
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) {
private _handleCut(e: Event) {
if (!shouldHandlerContinue(e, this._editor)) {
return;
}
const cur_select_info =
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;
}
this._dispatchClipboardEvent(ClipboardAction.CUT, e as ClipboardEvent);
}
private async paste_children(parent: AsyncBlock, children: any[]) {
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) {
private _preCopyCut(action: ClipboardAction, e: ClipboardEvent) {
switch (action) {
case ClipboardAction.COPY:
this.hooks.beforeCopy(e);
this._hooks.beforeCopy(e);
break;
case ClipboardAction.CUT:
this.hooks.beforeCut(e);
this._hooks.beforeCut(e);
break;
}
}
private dispatch_clipboard_event(
private _dispatchClipboardEvent(
action: ClipboardAction,
e: ClipboardEvent
) {
this.pre_copy_cut(action, e);
this._preCopyCut(action, e);
}
dispose() {
document.removeEventListener(ClipboardAction.COPY, this.handle_copy);
document.removeEventListener(ClipboardAction.CUT, this.handle_cut);
document.removeEventListener(ClipboardAction.PASTE, this.handle_paste);
this.event_target.removeEventListener(
ClipboardAction.COPY,
this.handle_copy
);
this.event_target.removeEventListener(
ClipboardAction.CUT,
this.handle_cut
);
this.event_target.removeEventListener(
document.removeEventListener(ClipboardAction.COPY, this._handleCopy);
document.removeEventListener(ClipboardAction.CUT, this._handleCut);
document.removeEventListener(
ClipboardAction.PASTE,
this.handle_paste
this._paste.handlePaste
);
this.clipboard_parse.dispose();
this.clipboard_parse = null;
this.event_target = null;
this.hooks = null;
this.editor = null;
this._eventTarget.removeEventListener(
ClipboardAction.COPY,
this._handleCopy
);
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;
}
}

View File

@@ -115,10 +115,8 @@ export default class ClipboardParse {
const block_utils = this.editor.getView(
ClipboardParse.block_types[i]
);
const blocks =
block_utils &&
block_utils.html2block &&
block_utils.html2block(el, this.parse_dom);
const blocks = block_utils?.html2block?.(el, this.parse_dom);
if (blocks) {
return blocks;
}

View File

@@ -46,7 +46,7 @@ class ClipboardPopulator {
}
// TODO: is not compatible with safari
const success = this.copy_to_cliboard_from_pc(clips);
const success = this._copyToClipboardFromPc(clips);
if (!success) {
// This way, not compatible with firefox
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;
const tempElem = document.createElement('textarea');
tempElem.value = 'temp';
@@ -96,56 +96,55 @@ class ClipboardPopulator {
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_view = this._editor.getView(block.type);
assert(block_view);
const block_info: ClipBlockInfo = {
const blockView = this._editor.getView(block.type);
assert(blockView);
const blockInfo: ClipBlockInfo = {
type: block.type,
properties: block_view.getSelProperties(block, selBlock),
properties: blockView.getSelProperties(block, selBlock),
children: [] as any[],
};
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]
);
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 select_info: SelectInfo =
const selectInfo: SelectInfo =
await this._selectionManager.getSelectInfo();
for (let i = 0; i < select_info.blocks.length; i++) {
const sel_block = select_info.blocks[i];
const clip_block_info = await this.get_clip_block_info(sel_block);
clips.push(clip_block_info);
for (let i = 0; i < selectInfo.blocks.length; i++) {
const selBlock = selectInfo.blocks[i];
const clipBlockInfo = await this._getClipBlockInfo(selBlock);
clips.push(clipBlockInfo);
}
const clipInfo: InnerClipInfo = {
select: select_info,
return {
select: selectInfo,
data: clips,
};
return clipInfo;
}
async getClips() {
const clips: any[] = [];
const inner_clip = await this.get_inner_clip();
const innerClip = await this._getInnerClip();
clips.push(
new Clip(
OFFICE_CLIPBOARD_MIMETYPE.DOCS_DOCUMENT_SLICE_CLIP_WRAPPED,
JSON.stringify(inner_clip)
JSON.stringify(innerClip)
)
);
const html_clip = await this._clipboardParse.generateHtml();
html_clip &&
clips.push(new Clip(OFFICE_CLIPBOARD_MIMETYPE.HTML, html_clip));
const htmlClip = await this._clipboardParse.generateHtml();
htmlClip &&
clips.push(new Clip(OFFICE_CLIPBOARD_MIMETYPE.HTML, htmlClip));
return clips;
}

View File

@@ -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, '&nbsp;');
// data = data.replace(/\t/g, '&nbsp;&nbsp;&nbsp;&nbsp;');
return data;
}
}

View File

@@ -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';
};

View File

@@ -30,6 +30,7 @@ import {
} from './types';
import { isLikeBlockListIds } from './utils';
import { Protocol } from '@toeverything/datasource/db-service';
import { Editor } from 'slate';
// IMP: maybe merge active and select into single function
export type SelectionInfo = InstanceType<
@@ -1047,4 +1048,25 @@ export class SelectionManager implements VirgoSelection {
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)
);
}
}