mirror of
https://github.com/toeverything/AFFiNE.git
synced 2024-12-18 20:51:46 +03:00
refactor: refactor paste behavior (not support selection)
This commit is contained in:
parent
de4b40605a
commit
0a265f9981
@ -0,0 +1,422 @@
|
|||||||
|
import { HooksRunner } from '../types';
|
||||||
|
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';
|
||||||
|
|
||||||
|
// todo needs to be a switch
|
||||||
|
const SUPPORT_MARKDOWN_PASTE = true;
|
||||||
|
|
||||||
|
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';
|
||||||
|
};
|
||||||
|
|
||||||
|
enum ClipboardAction {
|
||||||
|
COPY = 'copy',
|
||||||
|
CUT = 'cut',
|
||||||
|
PASTE = 'paste',
|
||||||
|
}
|
||||||
|
class BrowserClipboard {
|
||||||
|
private _eventTarget: Element;
|
||||||
|
private _hooks: HooksRunner;
|
||||||
|
private _editor: Editor;
|
||||||
|
private _clipboardParse: ClipboardParse;
|
||||||
|
private _markdownParse: MarkdownParser;
|
||||||
|
|
||||||
|
private static _optimalMimeType: string[] = [
|
||||||
|
OFFICE_CLIPBOARD_MIMETYPE.DOCS_DOCUMENT_SLICE_CLIP_WRAPPED,
|
||||||
|
OFFICE_CLIPBOARD_MIMETYPE.HTML,
|
||||||
|
OFFICE_CLIPBOARD_MIMETYPE.TEXT,
|
||||||
|
];
|
||||||
|
|
||||||
|
constructor(eventTarget: Element, hooks: HooksRunner, editor: Editor) {
|
||||||
|
this._eventTarget = eventTarget;
|
||||||
|
this._hooks = hooks;
|
||||||
|
this._editor = editor;
|
||||||
|
this._clipboardParse = new ClipboardParse(editor);
|
||||||
|
this._markdownParse = new MarkdownParser();
|
||||||
|
this._initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
public getClipboardParse() {
|
||||||
|
return this._clipboardParse;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _initialize() {
|
||||||
|
this._handleCopy = this._handleCopy.bind(this);
|
||||||
|
this._handleCut = this._handleCut.bind(this);
|
||||||
|
this._handlePaste = this._handlePaste.bind(this);
|
||||||
|
|
||||||
|
document.addEventListener(ClipboardAction.COPY, this._handleCopy);
|
||||||
|
document.addEventListener(ClipboardAction.CUT, this._handleCut);
|
||||||
|
document.addEventListener(ClipboardAction.PASTE, this._handlePaste);
|
||||||
|
this._eventTarget.addEventListener(
|
||||||
|
ClipboardAction.COPY,
|
||||||
|
this._handleCopy
|
||||||
|
);
|
||||||
|
this._eventTarget.addEventListener(
|
||||||
|
ClipboardAction.CUT,
|
||||||
|
this._handleCut
|
||||||
|
);
|
||||||
|
this._eventTarget.addEventListener(
|
||||||
|
ClipboardAction.PASTE,
|
||||||
|
this._handlePaste
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleCopy(e: Event) {
|
||||||
|
if (!shouldHandlerContinue(e, this._editor)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._dispatchClipboardEvent(ClipboardAction.COPY, e as ClipboardEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleCut(e: Event) {
|
||||||
|
if (!shouldHandlerContinue(e, this._editor)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._dispatchClipboardEvent(ClipboardAction.CUT, e as ClipboardEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handlePaste(e: Event) {
|
||||||
|
if (!shouldHandlerContinue(e, this._editor)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
const clipboardData = (e as ClipboardEvent).clipboardData;
|
||||||
|
|
||||||
|
const isPureFile = this._isPureFileInClipboard(clipboardData);
|
||||||
|
|
||||||
|
if (isPureFile) {
|
||||||
|
this._pasteFile(clipboardData);
|
||||||
|
} else {
|
||||||
|
this._pasteContent(clipboardData);
|
||||||
|
}
|
||||||
|
// this._editor.selectionManager
|
||||||
|
// .getSelectInfo()
|
||||||
|
// .then(selectionInfo => console.log(selectionInfo));
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = this._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 _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);
|
||||||
|
this.insert_blocks(blocks);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _pasteText(clipData: any, originTextClipData: any) {
|
||||||
|
const blocks = this._clipboardParse.text2blocks(clipData);
|
||||||
|
this.insert_blocks(blocks);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _pasteFile(clipboardData: any) {
|
||||||
|
const file = this._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[],
|
||||||
|
};
|
||||||
|
this.insert_blocks([blockInfo]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getImageFile(clipboardData: any) {
|
||||||
|
const files = clipboardData.files;
|
||||||
|
if (files && files[0] && files[0].type.indexOf('image') > -1) {
|
||||||
|
return files[0];
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _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;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getOptimalClip(clipboardData: any) {
|
||||||
|
const mimeTypeArr = BrowserClipboard._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 _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 async _firePasteEditAction(clipboardData: any) {
|
||||||
|
const clipInfo: InnerClipInfo = JSON.parse(clipboardData);
|
||||||
|
clipInfo && this.insert_blocks(clipInfo.data, clipInfo.select);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _canEditText(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;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cur_select_info =
|
||||||
|
await this._editor.selectionManager.getSelectInfo();
|
||||||
|
if (cur_select_info.blocks.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let beginIndex = 0;
|
||||||
|
const curNodeId =
|
||||||
|
cur_select_info.blocks[cur_select_info.blocks.length - 1].blockId;
|
||||||
|
let curBlock = await this._editor.getBlockById(curNodeId);
|
||||||
|
const blockView = this._editor.getView(curBlock.type);
|
||||||
|
if (
|
||||||
|
cur_select_info.type === 'Range' &&
|
||||||
|
curBlock.type === 'text' &&
|
||||||
|
blockView.isEmpty(curBlock)
|
||||||
|
) {
|
||||||
|
await curBlock.setType(blocks[0].type);
|
||||||
|
curBlock.setProperties(blocks[0].properties);
|
||||||
|
await this._pasteChildren(curBlock, blocks[0].children);
|
||||||
|
beginIndex = 1;
|
||||||
|
} else if (
|
||||||
|
select?.type === 'Range' &&
|
||||||
|
cur_select_info.type === 'Range' &&
|
||||||
|
this._canEditText(curBlock.type) &&
|
||||||
|
this._canEditText(blocks[0].type)
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
cur_select_info.blocks.length > 0 &&
|
||||||
|
cur_select_info.blocks[0].startInfo
|
||||||
|
) {
|
||||||
|
const startInfo = cur_select_info.blocks[0].startInfo;
|
||||||
|
const endInfo = cur_select_info.blocks[0].endInfo;
|
||||||
|
const curTextValue = curBlock.getProperty('text').value;
|
||||||
|
const pre_curTextValue = curTextValue.slice(
|
||||||
|
0,
|
||||||
|
startInfo.arrayIndex
|
||||||
|
);
|
||||||
|
const lastCurTextValue = curTextValue.slice(
|
||||||
|
endInfo.arrayIndex + 1
|
||||||
|
);
|
||||||
|
const preText = curTextValue[
|
||||||
|
startInfo.arrayIndex
|
||||||
|
].text.substring(0, startInfo.offset);
|
||||||
|
const lastText = curTextValue[
|
||||||
|
endInfo.arrayIndex
|
||||||
|
].text.substring(endInfo.offset);
|
||||||
|
|
||||||
|
let lastBlock: ClipBlockInfo = blocks[blocks.length - 1];
|
||||||
|
if (!this._canEditText(lastBlock.type)) {
|
||||||
|
lastBlock = { type: 'text', children: [] };
|
||||||
|
blocks.push(lastBlock);
|
||||||
|
}
|
||||||
|
const lastValues = lastBlock.properties?.text?.value;
|
||||||
|
lastText && lastValues.push({ text: lastText });
|
||||||
|
lastValues.push(...lastCurTextValue);
|
||||||
|
lastBlock.properties = {
|
||||||
|
text: { value: lastValues },
|
||||||
|
};
|
||||||
|
|
||||||
|
const insertInfo = blocks[0].properties.text;
|
||||||
|
preText && pre_curTextValue.push({ text: preText });
|
||||||
|
pre_curTextValue.push(...insertInfo.value);
|
||||||
|
this._editor.blockHelper.setBlockBlur(curNodeId);
|
||||||
|
setTimeout(async () => {
|
||||||
|
const curBlock = await this._editor.getBlockById(curNodeId);
|
||||||
|
curBlock.setProperties({
|
||||||
|
text: { value: pre_curTextValue },
|
||||||
|
});
|
||||||
|
await this._pasteChildren(curBlock, blocks[0].children);
|
||||||
|
}, 0);
|
||||||
|
beginIndex = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = beginIndex; i < blocks.length; i++) {
|
||||||
|
const nextBlock = await this._editor.createBlock(blocks[i].type);
|
||||||
|
nextBlock.setProperties(blocks[i].properties);
|
||||||
|
if (curBlock.type === 'page') {
|
||||||
|
curBlock.prepend(nextBlock);
|
||||||
|
} else {
|
||||||
|
curBlock.after(nextBlock);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this._pasteChildren(nextBlock, blocks[i].children);
|
||||||
|
curBlock = nextBlock;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _pasteChildren(parent: AsyncBlock, children: any[]) {
|
||||||
|
for (let i = 0; i < children.length; i++) {
|
||||||
|
const nextBlock = await this._editor.createBlock(children[i].type);
|
||||||
|
nextBlock.setProperties(children[i].properties);
|
||||||
|
await parent.append(nextBlock);
|
||||||
|
await this._pasteChildren(nextBlock, children[i].children);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _preCopyCut(action: ClipboardAction, e: ClipboardEvent) {
|
||||||
|
switch (action) {
|
||||||
|
case ClipboardAction.COPY:
|
||||||
|
this._hooks.beforeCopy(e);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ClipboardAction.CUT:
|
||||||
|
this._hooks.beforeCut(e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _dispatchClipboardEvent(
|
||||||
|
action: ClipboardAction,
|
||||||
|
e: ClipboardEvent
|
||||||
|
) {
|
||||||
|
this._preCopyCut(action, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose() {
|
||||||
|
document.removeEventListener(ClipboardAction.COPY, this._handleCopy);
|
||||||
|
document.removeEventListener(ClipboardAction.CUT, this._handleCut);
|
||||||
|
document.removeEventListener(ClipboardAction.PASTE, this._handlePaste);
|
||||||
|
this._eventTarget.removeEventListener(
|
||||||
|
ClipboardAction.COPY,
|
||||||
|
this._handleCopy
|
||||||
|
);
|
||||||
|
this._eventTarget.removeEventListener(
|
||||||
|
ClipboardAction.CUT,
|
||||||
|
this._handleCut
|
||||||
|
);
|
||||||
|
this._eventTarget.removeEventListener(
|
||||||
|
ClipboardAction.PASTE,
|
||||||
|
this._handlePaste
|
||||||
|
);
|
||||||
|
this._clipboardParse.dispose();
|
||||||
|
this._clipboardParse = null;
|
||||||
|
this._eventTarget = null;
|
||||||
|
this._hooks = null;
|
||||||
|
this._editor = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { BrowserClipboard };
|
@ -14,40 +14,24 @@ 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;
|
|
||||||
|
|
||||||
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';
|
|
||||||
};
|
|
||||||
|
|
||||||
enum ClipboardAction {
|
enum ClipboardAction {
|
||||||
COPY = 'copy',
|
COPY = 'copy',
|
||||||
CUT = 'cut',
|
CUT = 'cut',
|
||||||
PASTE = 'paste',
|
PASTE = 'paste',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//TODO: need to consider the cursor position after inserting the children
|
||||||
class BrowserClipboard {
|
class BrowserClipboard {
|
||||||
private _eventTarget: Element;
|
private _eventTarget: Element;
|
||||||
private _hooks: HooksRunner;
|
private _hooks: HooksRunner;
|
||||||
private _editor: Editor;
|
private _editor: Editor;
|
||||||
private _clipboardParse: ClipboardParse;
|
private _clipboardParse: ClipboardParse;
|
||||||
private _markdownParse: MarkdownParser;
|
private _markdownParse: MarkdownParser;
|
||||||
|
private _paste: Paste;
|
||||||
private static _optimalMimeType: string[] = [
|
|
||||||
OFFICE_CLIPBOARD_MIMETYPE.DOCS_DOCUMENT_SLICE_CLIP_WRAPPED,
|
|
||||||
OFFICE_CLIPBOARD_MIMETYPE.HTML,
|
|
||||||
OFFICE_CLIPBOARD_MIMETYPE.TEXT,
|
|
||||||
];
|
|
||||||
|
|
||||||
constructor(eventTarget: Element, hooks: HooksRunner, editor: Editor) {
|
constructor(eventTarget: Element, hooks: HooksRunner, editor: Editor) {
|
||||||
this._eventTarget = eventTarget;
|
this._eventTarget = eventTarget;
|
||||||
@ -55,6 +39,11 @@ class BrowserClipboard {
|
|||||||
this._editor = editor;
|
this._editor = editor;
|
||||||
this._clipboardParse = new ClipboardParse(editor);
|
this._clipboardParse = new ClipboardParse(editor);
|
||||||
this._markdownParse = new MarkdownParser();
|
this._markdownParse = new MarkdownParser();
|
||||||
|
this._paste = new Paste(
|
||||||
|
editor,
|
||||||
|
this._clipboardParse,
|
||||||
|
this._markdownParse
|
||||||
|
);
|
||||||
this._initialize();
|
this._initialize();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,11 +54,13 @@ class BrowserClipboard {
|
|||||||
private _initialize() {
|
private _initialize() {
|
||||||
this._handleCopy = this._handleCopy.bind(this);
|
this._handleCopy = this._handleCopy.bind(this);
|
||||||
this._handleCut = this._handleCut.bind(this);
|
this._handleCut = this._handleCut.bind(this);
|
||||||
this._handlePaste = this._handlePaste.bind(this);
|
|
||||||
|
|
||||||
document.addEventListener(ClipboardAction.COPY, this._handleCopy);
|
document.addEventListener(ClipboardAction.COPY, this._handleCopy);
|
||||||
document.addEventListener(ClipboardAction.CUT, this._handleCut);
|
document.addEventListener(ClipboardAction.CUT, this._handleCut);
|
||||||
document.addEventListener(ClipboardAction.PASTE, this._handlePaste);
|
document.addEventListener(
|
||||||
|
ClipboardAction.PASTE,
|
||||||
|
this._paste.handlePaste
|
||||||
|
);
|
||||||
this._eventTarget.addEventListener(
|
this._eventTarget.addEventListener(
|
||||||
ClipboardAction.COPY,
|
ClipboardAction.COPY,
|
||||||
this._handleCopy
|
this._handleCopy
|
||||||
@ -80,7 +71,7 @@ class BrowserClipboard {
|
|||||||
);
|
);
|
||||||
this._eventTarget.addEventListener(
|
this._eventTarget.addEventListener(
|
||||||
ClipboardAction.PASTE,
|
ClipboardAction.PASTE,
|
||||||
this._handlePaste
|
this._paste.handlePaste
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,282 +91,6 @@ class BrowserClipboard {
|
|||||||
this._dispatchClipboardEvent(ClipboardAction.CUT, e as ClipboardEvent);
|
this._dispatchClipboardEvent(ClipboardAction.CUT, e as ClipboardEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _handlePaste(e: Event) {
|
|
||||||
if (!shouldHandlerContinue(e, this._editor)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
const clipboardData = (e as ClipboardEvent).clipboardData;
|
|
||||||
|
|
||||||
const isPureFile = this._isPureFileInClipboard(clipboardData);
|
|
||||||
|
|
||||||
if (isPureFile) {
|
|
||||||
this._pasteFile(clipboardData);
|
|
||||||
} else {
|
|
||||||
this._pasteContent(clipboardData);
|
|
||||||
}
|
|
||||||
// this._editor.selectionManager
|
|
||||||
// .getSelectInfo()
|
|
||||||
// .then(selectionInfo => console.log(selectionInfo));
|
|
||||||
}
|
|
||||||
|
|
||||||
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 = this._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 _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);
|
|
||||||
this.insert_blocks(blocks);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _pasteText(clipData: any, originTextClipData: any) {
|
|
||||||
const blocks = this._clipboardParse.text2blocks(clipData);
|
|
||||||
this.insert_blocks(blocks);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _pasteFile(clipboardData: any) {
|
|
||||||
const file = this._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[],
|
|
||||||
};
|
|
||||||
this.insert_blocks([blockInfo]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _getImageFile(clipboardData: any) {
|
|
||||||
const files = clipboardData.files;
|
|
||||||
if (files && files[0] && files[0].type.indexOf('image') > -1) {
|
|
||||||
return files[0];
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _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;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getOptimalClip(clipboardData: any) {
|
|
||||||
const mimeTypeArr = BrowserClipboard._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 _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 async _firePasteEditAction(clipboardData: any) {
|
|
||||||
const clipInfo: InnerClipInfo = JSON.parse(clipboardData);
|
|
||||||
clipInfo && this.insert_blocks(clipInfo.data, clipInfo.select);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _canEditText(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;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cur_select_info =
|
|
||||||
await this._editor.selectionManager.getSelectInfo();
|
|
||||||
if (cur_select_info.blocks.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let beginIndex = 0;
|
|
||||||
const curNodeId =
|
|
||||||
cur_select_info.blocks[cur_select_info.blocks.length - 1].blockId;
|
|
||||||
let curBlock = await this._editor.getBlockById(curNodeId);
|
|
||||||
const blockView = this._editor.getView(curBlock.type);
|
|
||||||
if (
|
|
||||||
cur_select_info.type === 'Range' &&
|
|
||||||
curBlock.type === 'text' &&
|
|
||||||
blockView.isEmpty(curBlock)
|
|
||||||
) {
|
|
||||||
await curBlock.setType(blocks[0].type);
|
|
||||||
curBlock.setProperties(blocks[0].properties);
|
|
||||||
await this._pasteChildren(curBlock, blocks[0].children);
|
|
||||||
beginIndex = 1;
|
|
||||||
} else if (
|
|
||||||
select?.type === 'Range' &&
|
|
||||||
cur_select_info.type === 'Range' &&
|
|
||||||
this._canEditText(curBlock.type) &&
|
|
||||||
this._canEditText(blocks[0].type)
|
|
||||||
) {
|
|
||||||
if (
|
|
||||||
cur_select_info.blocks.length > 0 &&
|
|
||||||
cur_select_info.blocks[0].startInfo
|
|
||||||
) {
|
|
||||||
const startInfo = cur_select_info.blocks[0].startInfo;
|
|
||||||
const endInfo = cur_select_info.blocks[0].endInfo;
|
|
||||||
const curTextValue = curBlock.getProperty('text').value;
|
|
||||||
const pre_curTextValue = curTextValue.slice(
|
|
||||||
0,
|
|
||||||
startInfo.arrayIndex
|
|
||||||
);
|
|
||||||
const lastCurTextValue = curTextValue.slice(
|
|
||||||
endInfo.arrayIndex + 1
|
|
||||||
);
|
|
||||||
const preText = curTextValue[
|
|
||||||
startInfo.arrayIndex
|
|
||||||
].text.substring(0, startInfo.offset);
|
|
||||||
const lastText = curTextValue[
|
|
||||||
endInfo.arrayIndex
|
|
||||||
].text.substring(endInfo.offset);
|
|
||||||
|
|
||||||
let lastBlock: ClipBlockInfo = blocks[blocks.length - 1];
|
|
||||||
if (!this._canEditText(lastBlock.type)) {
|
|
||||||
lastBlock = { type: 'text', children: [] };
|
|
||||||
blocks.push(lastBlock);
|
|
||||||
}
|
|
||||||
const lastValues = lastBlock.properties?.text?.value;
|
|
||||||
lastText && lastValues.push({ text: lastText });
|
|
||||||
lastValues.push(...lastCurTextValue);
|
|
||||||
lastBlock.properties = {
|
|
||||||
text: { value: lastValues },
|
|
||||||
};
|
|
||||||
|
|
||||||
const insertInfo = blocks[0].properties.text;
|
|
||||||
preText && pre_curTextValue.push({ text: preText });
|
|
||||||
pre_curTextValue.push(...insertInfo.value);
|
|
||||||
this._editor.blockHelper.setBlockBlur(curNodeId);
|
|
||||||
setTimeout(async () => {
|
|
||||||
const curBlock = await this._editor.getBlockById(curNodeId);
|
|
||||||
curBlock.setProperties({
|
|
||||||
text: { value: pre_curTextValue },
|
|
||||||
});
|
|
||||||
await this._pasteChildren(curBlock, blocks[0].children);
|
|
||||||
}, 0);
|
|
||||||
beginIndex = 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = beginIndex; i < blocks.length; i++) {
|
|
||||||
const nextBlock = await this._editor.createBlock(blocks[i].type);
|
|
||||||
nextBlock.setProperties(blocks[i].properties);
|
|
||||||
if (curBlock.type === 'page') {
|
|
||||||
curBlock.prepend(nextBlock);
|
|
||||||
} else {
|
|
||||||
curBlock.after(nextBlock);
|
|
||||||
}
|
|
||||||
|
|
||||||
await this._pasteChildren(nextBlock, blocks[i].children);
|
|
||||||
curBlock = nextBlock;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _pasteChildren(parent: AsyncBlock, children: any[]) {
|
|
||||||
for (let i = 0; i < children.length; i++) {
|
|
||||||
const nextBlock = await this._editor.createBlock(children[i].type);
|
|
||||||
nextBlock.setProperties(children[i].properties);
|
|
||||||
await parent.append(nextBlock);
|
|
||||||
await this._pasteChildren(nextBlock, children[i].children);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _preCopyCut(action: ClipboardAction, e: ClipboardEvent) {
|
private _preCopyCut(action: ClipboardAction, e: ClipboardEvent) {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case ClipboardAction.COPY:
|
case ClipboardAction.COPY:
|
||||||
@ -398,7 +113,10 @@ class BrowserClipboard {
|
|||||||
dispose() {
|
dispose() {
|
||||||
document.removeEventListener(ClipboardAction.COPY, this._handleCopy);
|
document.removeEventListener(ClipboardAction.COPY, this._handleCopy);
|
||||||
document.removeEventListener(ClipboardAction.CUT, this._handleCut);
|
document.removeEventListener(ClipboardAction.CUT, this._handleCut);
|
||||||
document.removeEventListener(ClipboardAction.PASTE, this._handlePaste);
|
document.removeEventListener(
|
||||||
|
ClipboardAction.PASTE,
|
||||||
|
this._paste.handlePaste
|
||||||
|
);
|
||||||
this._eventTarget.removeEventListener(
|
this._eventTarget.removeEventListener(
|
||||||
ClipboardAction.COPY,
|
ClipboardAction.COPY,
|
||||||
this._handleCopy
|
this._handleCopy
|
||||||
@ -409,7 +127,7 @@ class BrowserClipboard {
|
|||||||
);
|
);
|
||||||
this._eventTarget.removeEventListener(
|
this._eventTarget.removeEventListener(
|
||||||
ClipboardAction.PASTE,
|
ClipboardAction.PASTE,
|
||||||
this._handlePaste
|
this._paste.handlePaste
|
||||||
);
|
);
|
||||||
this._clipboardParse.dispose();
|
this._clipboardParse.dispose();
|
||||||
this._clipboardParse = null;
|
this._clipboardParse = null;
|
||||||
|
535
libs/components/editor-core/src/editor/clipboard/paste.ts
Normal file
535
libs/components/editor-core/src/editor/clipboard/paste.ts
Normal file
@ -0,0 +1,535 @@
|
|||||||
|
import { HooksRunner } from '../types';
|
||||||
|
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();
|
||||||
|
|
||||||
|
// 当选区在某一个block中时
|
||||||
|
// select?.type === 'Range'
|
||||||
|
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: '' }];
|
||||||
|
this._editor.blockHelper.setBlockBlur(
|
||||||
|
currentSelectInfo.blocks[0].blockId
|
||||||
|
);
|
||||||
|
|
||||||
|
const { startInfo, endInfo } = currentSelectInfo.blocks[0];
|
||||||
|
|
||||||
|
// 选中的当前的可编辑block的文字信息
|
||||||
|
const currentTextValue =
|
||||||
|
selectedBlock.getProperty('text').value;
|
||||||
|
// 当光标选区跨越不同样式文字时
|
||||||
|
if (startInfo?.arrayIndex !== endInfo?.arrayIndex) {
|
||||||
|
if (shouldSplitBlock) {
|
||||||
|
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 {
|
||||||
|
const newTextValue = currentTextValue.reduce(
|
||||||
|
(
|
||||||
|
newTextValue: TextValueItem[],
|
||||||
|
textStore: TextValueItem,
|
||||||
|
i: number
|
||||||
|
) => {
|
||||||
|
if (
|
||||||
|
i < startInfo?.arrayIndex ||
|
||||||
|
i > endInfo?.arrayIndex
|
||||||
|
) {
|
||||||
|
newTextValue.push(textStore);
|
||||||
|
}
|
||||||
|
const { text, ...props } = textStore;
|
||||||
|
|
||||||
|
if (i === startInfo?.arrayIndex) {
|
||||||
|
newTextValue.push({
|
||||||
|
text: text.slice(0, startInfo?.offset),
|
||||||
|
...props,
|
||||||
|
});
|
||||||
|
} else if (i === endInfo?.arrayIndex) {
|
||||||
|
newTextValue.push({
|
||||||
|
text: text.slice(endInfo?.offset),
|
||||||
|
...props,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return newTextValue;
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
newTextValue.splice(
|
||||||
|
startInfo?.arrayIndex + 1,
|
||||||
|
0,
|
||||||
|
...pureText
|
||||||
|
);
|
||||||
|
selectedBlock.setProperties({
|
||||||
|
text: {
|
||||||
|
value: newTextValue,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 当光标选区没有跨越不同样式文字时
|
||||||
|
if (startInfo?.arrayIndex === endInfo?.arrayIndex) {
|
||||||
|
if (shouldSplitBlock) {
|
||||||
|
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 {
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
newTextValue.push(...pureText);
|
||||||
|
|
||||||
|
newTextValue.push({
|
||||||
|
text: `${text.slice(endInfo?.offset)}`,
|
||||||
|
...props,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return newTextValue;
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
selectedBlock.setProperties({
|
||||||
|
text: {
|
||||||
|
value: newTextValue,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const pastedTextLength = pureText.reduce(
|
||||||
|
(sumLength: number, textItem: TextValueItem) => {
|
||||||
|
sumLength += textItem.text.length;
|
||||||
|
return sumLength;
|
||||||
|
},
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
// this._editor.selectionManager.moveCursor(
|
||||||
|
// window.getSelection().getRangeAt(0),
|
||||||
|
// pastedTextLength,
|
||||||
|
// selectedBlock.id
|
||||||
|
// );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} 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;
|
||||||
|
}
|
||||||
|
}
|
14
libs/components/editor-core/src/editor/clipboard/utils.ts
Normal file
14
libs/components/editor-core/src/editor/clipboard/utils.ts
Normal 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';
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user