refactor: refactor paste behavior (not support selection)

This commit is contained in:
QiShaoXuan 2022-08-11 17:06:16 +08:00
parent de4b40605a
commit 0a265f9981
4 changed files with 991 additions and 302 deletions

View File

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

View File

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

View 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, '&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';
};