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