diff --git a/libs/components/editor-core/src/editor/clipboard/browser-clipboard-back.ts b/libs/components/editor-core/src/editor/clipboard/browser-clipboard-back.ts
new file mode 100644
index 0000000000..6c6d4ad031
--- /dev/null
+++ b/libs/components/editor-core/src/editor/clipboard/browser-clipboard-back.ts
@@ -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, '
');
+ }
+
+ // 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 };
diff --git a/libs/components/editor-core/src/editor/clipboard/browser-clipboard.ts b/libs/components/editor-core/src/editor/clipboard/browser-clipboard.ts
index 6c6d4ad031..64eecbc5ca 100644
--- a/libs/components/editor-core/src/editor/clipboard/browser-clipboard.ts
+++ b/libs/components/editor-core/src/editor/clipboard/browser-clipboard.ts
@@ -14,40 +14,24 @@ import {
services,
} from '@toeverything/datasource/db-service';
import { MarkdownParser } from './markdown-parse';
-
+import { shouldHandlerContinue } from './utils';
+import { Paste } from './paste';
// todo needs to be a switch
-const SUPPORT_MARKDOWN_PASTE = true;
-
-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',
}
+
+//TODO: need to consider the cursor position after inserting the children
class BrowserClipboard {
private _eventTarget: Element;
private _hooks: HooksRunner;
private _editor: Editor;
private _clipboardParse: ClipboardParse;
private _markdownParse: MarkdownParser;
-
- private static _optimalMimeType: string[] = [
- OFFICE_CLIPBOARD_MIMETYPE.DOCS_DOCUMENT_SLICE_CLIP_WRAPPED,
- OFFICE_CLIPBOARD_MIMETYPE.HTML,
- OFFICE_CLIPBOARD_MIMETYPE.TEXT,
- ];
+ private _paste: Paste;
constructor(eventTarget: Element, hooks: HooksRunner, editor: Editor) {
this._eventTarget = eventTarget;
@@ -55,6 +39,11 @@ class BrowserClipboard {
this._editor = editor;
this._clipboardParse = new ClipboardParse(editor);
this._markdownParse = new MarkdownParser();
+ this._paste = new Paste(
+ editor,
+ this._clipboardParse,
+ this._markdownParse
+ );
this._initialize();
}
@@ -65,11 +54,13 @@ class BrowserClipboard {
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);
+ document.addEventListener(
+ ClipboardAction.PASTE,
+ this._paste.handlePaste
+ );
this._eventTarget.addEventListener(
ClipboardAction.COPY,
this._handleCopy
@@ -80,7 +71,7 @@ class BrowserClipboard {
);
this._eventTarget.addEventListener(
ClipboardAction.PASTE,
- this._handlePaste
+ this._paste.handlePaste
);
}
@@ -100,282 +91,6 @@ class BrowserClipboard {
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, '
');
- }
-
- // 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:
@@ -398,7 +113,10 @@ class BrowserClipboard {
dispose() {
document.removeEventListener(ClipboardAction.COPY, this._handleCopy);
document.removeEventListener(ClipboardAction.CUT, this._handleCut);
- document.removeEventListener(ClipboardAction.PASTE, this._handlePaste);
+ document.removeEventListener(
+ ClipboardAction.PASTE,
+ this._paste.handlePaste
+ );
this._eventTarget.removeEventListener(
ClipboardAction.COPY,
this._handleCopy
@@ -409,7 +127,7 @@ class BrowserClipboard {
);
this._eventTarget.removeEventListener(
ClipboardAction.PASTE,
- this._handlePaste
+ this._paste.handlePaste
);
this._clipboardParse.dispose();
this._clipboardParse = null;
diff --git a/libs/components/editor-core/src/editor/clipboard/paste.ts b/libs/components/editor-core/src/editor/clipboard/paste.ts
new file mode 100644
index 0000000000..022b3de214
--- /dev/null
+++ b/libs/components/editor-core/src/editor/clipboard/paste.ts
@@ -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, '
');
+ }
+
+ // data = data.replace(/ /g, ' ');
+ // data = data.replace(/\t/g, ' ');
+ return data;
+ }
+}
diff --git a/libs/components/editor-core/src/editor/clipboard/utils.ts b/libs/components/editor-core/src/editor/clipboard/utils.ts
new file mode 100644
index 0000000000..7b4d420a3a
--- /dev/null
+++ b/libs/components/editor-core/src/editor/clipboard/utils.ts
@@ -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';
+};