From eee90f1a5c23a3474e752427e0012f4ce5120704 Mon Sep 17 00:00:00 2001 From: QiShaoXuan Date: Thu, 4 Aug 2022 19:18:45 +0800 Subject: [PATCH 01/12] feat: improved clipboard event determination conditions --- .../src/editor/clipboard/browser-clipboard.ts | 31 ++++++++++++------- .../src/editor/clipboard/clipboard-parse.ts | 6 ++-- 2 files changed, 22 insertions(+), 15 deletions(-) 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 eefca42b0..b3dab25ca 100644 --- a/libs/components/editor-core/src/editor/clipboard/browser-clipboard.ts +++ b/libs/components/editor-core/src/editor/clipboard/browser-clipboard.ts @@ -17,6 +17,18 @@ import { MarkdownParser } from './markdown-parse'; // todo needs to be a switch const support_markdown_paste = true; +const filterNodes = ['INPUT', 'SELECT', 'TEXTAREA']; + +const shouldHandlerContinue = (event: Event, editor: Editor) => { + 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', @@ -72,8 +84,7 @@ class BrowserClipboard { } private handle_copy(e: Event) { - //@ts-ignore - if (e.defaultPrevented || e.target.nodeName === 'INPUT') { + if (!shouldHandlerContinue(e, this.editor)) { return; } @@ -84,33 +95,31 @@ class BrowserClipboard { } private handle_cut(e: Event) { - //@ts-ignore - if (e.defaultPrevented || e.target.nodeName === 'INPUT') { + if (!shouldHandlerContinue(e, this.editor)) { return; } + this.dispatch_clipboard_event(ClipboardAction.CUT, e as ClipboardEvent); } private handle_paste(e: Event) { - //@ts-ignore TODO should be handled more scientifically here, whether to trigger the paste time, also need some whitelist mechanism - if (e.defaultPrevented || e.target.nodeName === 'INPUT') { + if (!shouldHandlerContinue(e, this.editor)) { return; } + e.stopPropagation(); const clipboardData = (e as ClipboardEvent).clipboardData; const isPureFile = this.is_pure_file_in_clipboard(clipboardData); - if (!isPureFile) { - this.paste_content(clipboardData); - } else { + if (isPureFile) { this.paste_file(clipboardData); + } else { + this.paste_content(clipboardData); } // this.editor.selectionManager // .getSelectInfo() // .then(selectionInfo => console.log(selectionInfo)); - - e.stopPropagation(); } private paste_content(clipboardData: any) { diff --git a/libs/components/editor-core/src/editor/clipboard/clipboard-parse.ts b/libs/components/editor-core/src/editor/clipboard/clipboard-parse.ts index a4a59bd2b..43b72ed6e 100644 --- a/libs/components/editor-core/src/editor/clipboard/clipboard-parse.ts +++ b/libs/components/editor-core/src/editor/clipboard/clipboard-parse.ts @@ -115,10 +115,8 @@ export default class ClipboardParse { const block_utils = this.editor.getView( ClipboardParse.block_types[i] ); - const blocks = - block_utils && - block_utils.html2block && - block_utils.html2block(el, this.parse_dom); + const blocks = block_utils?.html2block?.(el, this.parse_dom); + if (blocks) { return blocks; } From 75083f1c75bebec7c670513ee9c7854533245d83 Mon Sep 17 00:00:00 2001 From: QiShaoXuan Date: Tue, 9 Aug 2022 14:54:59 +0800 Subject: [PATCH 02/12] style: update code style --- .../src/editor/clipboard/browser-clipboard.ts | 300 +++++++++--------- 1 file changed, 148 insertions(+), 152 deletions(-) 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 b3dab25ca..6c6d4ad03 100644 --- a/libs/components/editor-core/src/editor/clipboard/browser-clipboard.ts +++ b/libs/components/editor-core/src/editor/clipboard/browser-clipboard.ts @@ -16,10 +16,11 @@ import { import { MarkdownParser } from './markdown-parse'; // todo needs to be a switch -const support_markdown_paste = true; -const filterNodes = ['INPUT', 'SELECT', 'TEXTAREA']; +const SUPPORT_MARKDOWN_PASTE = true; const shouldHandlerContinue = (event: Event, editor: Editor) => { + const filterNodes = ['INPUT', 'SELECT', 'TEXTAREA']; + if (event.defaultPrevented) { return false; } @@ -36,96 +37,94 @@ enum ClipboardAction { PASTE = 'paste', } class BrowserClipboard { - private event_target: Element; - private hooks: HooksRunner; - private editor: Editor; - private clipboard_parse: ClipboardParse; - private markdown_parse: MarkdownParser; + private _eventTarget: Element; + private _hooks: HooksRunner; + private _editor: Editor; + private _clipboardParse: ClipboardParse; + private _markdownParse: MarkdownParser; - private static optimal_mime_type: string[] = [ + 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.event_target = eventTarget; - this.hooks = hooks; - this.editor = editor; - this.clipboard_parse = new ClipboardParse(editor); - this.markdown_parse = new MarkdownParser(); - this.initialize(); + this._eventTarget = eventTarget; + this._hooks = hooks; + this._editor = editor; + this._clipboardParse = new ClipboardParse(editor); + this._markdownParse = new MarkdownParser(); + this._initialize(); } public getClipboardParse() { - return this.clipboard_parse; + return this._clipboardParse; } - private initialize() { - this.handle_copy = this.handle_copy.bind(this); - this.handle_cut = this.handle_cut.bind(this); - this.handle_paste = this.handle_paste.bind(this); + private _initialize() { + this._handleCopy = this._handleCopy.bind(this); + this._handleCut = this._handleCut.bind(this); + this._handlePaste = this._handlePaste.bind(this); - document.addEventListener(ClipboardAction.COPY, this.handle_copy); - document.addEventListener(ClipboardAction.CUT, this.handle_cut); - document.addEventListener(ClipboardAction.PASTE, this.handle_paste); - this.event_target.addEventListener( + document.addEventListener(ClipboardAction.COPY, this._handleCopy); + document.addEventListener(ClipboardAction.CUT, this._handleCut); + document.addEventListener(ClipboardAction.PASTE, this._handlePaste); + this._eventTarget.addEventListener( ClipboardAction.COPY, - this.handle_copy + this._handleCopy ); - this.event_target.addEventListener( + this._eventTarget.addEventListener( ClipboardAction.CUT, - this.handle_cut + this._handleCut ); - this.event_target.addEventListener( + this._eventTarget.addEventListener( ClipboardAction.PASTE, - this.handle_paste + this._handlePaste ); } - private handle_copy(e: Event) { - if (!shouldHandlerContinue(e, this.editor)) { + private _handleCopy(e: Event) { + if (!shouldHandlerContinue(e, this._editor)) { return; } - this.dispatch_clipboard_event( - ClipboardAction.COPY, - e as ClipboardEvent - ); + this._dispatchClipboardEvent(ClipboardAction.COPY, e as ClipboardEvent); } - private handle_cut(e: Event) { - if (!shouldHandlerContinue(e, this.editor)) { + private _handleCut(e: Event) { + if (!shouldHandlerContinue(e, this._editor)) { return; } - this.dispatch_clipboard_event(ClipboardAction.CUT, e as ClipboardEvent); + this._dispatchClipboardEvent(ClipboardAction.CUT, e as ClipboardEvent); } - private handle_paste(e: Event) { - if (!shouldHandlerContinue(e, this.editor)) { + private _handlePaste(e: Event) { + if (!shouldHandlerContinue(e, this._editor)) { return; } e.stopPropagation(); const clipboardData = (e as ClipboardEvent).clipboardData; - const isPureFile = this.is_pure_file_in_clipboard(clipboardData); + const isPureFile = this._isPureFileInClipboard(clipboardData); if (isPureFile) { - this.paste_file(clipboardData); + this._pasteFile(clipboardData); } else { - this.paste_content(clipboardData); + this._pasteContent(clipboardData); } - // this.editor.selectionManager + // this._editor.selectionManager // .getSelectInfo() // .then(selectionInfo => console.log(selectionInfo)); } - private paste_content(clipboardData: any) { + 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 ); @@ -133,19 +132,19 @@ class BrowserClipboard { let clipData = originClip['data']; if (originClip['type'] === OFFICE_CLIPBOARD_MIMETYPE.TEXT) { - clipData = this.excape_html(clipData); + clipData = this._excapeHtml(clipData); } switch (originClip['type']) { /** Protocol paste */ case OFFICE_CLIPBOARD_MIMETYPE.DOCS_DOCUMENT_SLICE_CLIP_WRAPPED: - this.fire_paste_edit_action(clipData); + this._firePasteEditAction(clipData); break; case OFFICE_CLIPBOARD_MIMETYPE.HTML: - this.paste_html(clipData, originTextClipData); + this._pasteHtml(clipData, originTextClipData); break; case OFFICE_CLIPBOARD_MIMETYPE.TEXT: - this.paste_text(clipData, originTextClipData); + this._pasteText(clipData, originTextClipData); break; default: @@ -153,14 +152,14 @@ class BrowserClipboard { } } - private paste_html(clipData: any, originTextClipData: any) { - if (support_markdown_paste) { - const has_markdown = - this.markdown_parse.checkIfTextContainsMd(originTextClipData); - if (has_markdown) { + private _pasteHtml(clipData: any, originTextClipData: any) { + if (SUPPORT_MARKDOWN_PASTE) { + const hasMarkdown = + this._markdownParse.checkIfTextContainsMd(originTextClipData); + if (hasMarkdown) { try { const convertedDataObj = - this.markdown_parse.md2Html(originTextClipData); + this._markdownParse.md2Html(originTextClipData); if (convertedDataObj.isConverted) { clipData = convertedDataObj.text; } @@ -171,23 +170,23 @@ class BrowserClipboard { } } - const blocks = this.clipboard_parse.html2blocks(clipData); + const blocks = this._clipboardParse.html2blocks(clipData); this.insert_blocks(blocks); } - private paste_text(clipData: any, originTextClipData: any) { - const blocks = this.clipboard_parse.text2blocks(clipData); + private _pasteText(clipData: any, originTextClipData: any) { + const blocks = this._clipboardParse.text2blocks(clipData); this.insert_blocks(blocks); } - private async paste_file(clipboardData: any) { - const file = this.get_image_file(clipboardData); + private async _pasteFile(clipboardData: any) { + const file = this._getImageFile(clipboardData); if (file) { const result = await services.api.file.create({ - workspace: this.editor.workspace, + workspace: this._editor.workspace, file: file, }); - const block_info: ClipBlockInfo = { + const blockInfo: ClipBlockInfo = { type: 'image', properties: { image: { @@ -199,11 +198,11 @@ class BrowserClipboard { }, children: [] as ClipBlockInfo[], }; - this.insert_blocks([block_info]); + this.insert_blocks([blockInfo]); } } - private get_image_file(clipboardData: any) { + private _getImageFile(clipboardData: any) { const files = clipboardData.files; if (files && files[0] && files[0].type.indexOf('image') > -1) { return files[0]; @@ -211,7 +210,7 @@ class BrowserClipboard { return; } - private excape_html(data: any, onlySpace?: any) { + private _excapeHtml(data: any, onlySpace?: any) { if (!onlySpace) { // TODO: // data = string.htmlEscape(data); @@ -224,7 +223,7 @@ class BrowserClipboard { } public getOptimalClip(clipboardData: any) { - const mimeTypeArr = BrowserClipboard.optimal_mime_type; + const mimeTypeArr = BrowserClipboard._optimalMimeType; for (let i = 0; i < mimeTypeArr.length; i++) { const data = @@ -242,24 +241,23 @@ class BrowserClipboard { return ''; } - private is_pure_file_in_clipboard(clipboardData: DataTransfer) { + private _isPureFileInClipboard(clipboardData: DataTransfer) { const types = clipboardData.types; - const res = + return ( (types.length === 1 && types[0] === 'Files') || (types.length === 2 && (types.includes('text/plain') || types.includes('text/html')) && - types.includes('Files')); - - return res; + types.includes('Files')) + ); } - private async fire_paste_edit_action(clipboardData: any) { - const clip_info: InnerClipInfo = JSON.parse(clipboardData); - clip_info && this.insert_blocks(clip_info.data, clip_info.select); + private async _firePasteEditAction(clipboardData: any) { + const clipInfo: InnerClipInfo = JSON.parse(clipboardData); + clipInfo && this.insert_blocks(clipInfo.data, clipInfo.select); } - private can_edit_text(type: BlockFlavorKeys) { + private _canEditText(type: BlockFlavorKeys) { return ( type === Protocol.Block.Type.page || type === Protocol.Block.Type.text || @@ -282,144 +280,142 @@ class BrowserClipboard { } const cur_select_info = - await this.editor.selectionManager.getSelectInfo(); + await this._editor.selectionManager.getSelectInfo(); if (cur_select_info.blocks.length === 0) { return; } - let begin_index = 0; - const cur_node_id = + let beginIndex = 0; + const curNodeId = cur_select_info.blocks[cur_select_info.blocks.length - 1].blockId; - let cur_block = await this.editor.getBlockById(cur_node_id); - const block_view = this.editor.getView(cur_block.type); + let curBlock = await this._editor.getBlockById(curNodeId); + const blockView = this._editor.getView(curBlock.type); if ( cur_select_info.type === 'Range' && - cur_block.type === 'text' && - block_view.isEmpty(cur_block) + curBlock.type === 'text' && + blockView.isEmpty(curBlock) ) { - cur_block.setType(blocks[0].type); - cur_block.setProperties(blocks[0].properties); - this.paste_children(cur_block, blocks[0].children); - begin_index = 1; + 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.can_edit_text(cur_block.type) && - this.can_edit_text(blocks[0].type) + this._canEditText(curBlock.type) && + this._canEditText(blocks[0].type) ) { if ( cur_select_info.blocks.length > 0 && cur_select_info.blocks[0].startInfo ) { - const start_info = cur_select_info.blocks[0].startInfo; - const end_info = cur_select_info.blocks[0].endInfo; - const cur_text_value = cur_block.getProperty('text').value; - const pre_cur_text_value = cur_text_value.slice( + 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, - start_info.arrayIndex + startInfo.arrayIndex ); - const last_cur_text_value = cur_text_value.slice( - end_info.arrayIndex + 1 + const lastCurTextValue = curTextValue.slice( + endInfo.arrayIndex + 1 ); - const pre_text = cur_text_value[ - start_info.arrayIndex - ].text.substring(0, start_info.offset); - const last_text = cur_text_value[ - end_info.arrayIndex - ].text.substring(end_info.offset); + const preText = curTextValue[ + startInfo.arrayIndex + ].text.substring(0, startInfo.offset); + const lastText = curTextValue[ + endInfo.arrayIndex + ].text.substring(endInfo.offset); - let last_block: ClipBlockInfo = blocks[blocks.length - 1]; - if (!this.can_edit_text(last_block.type)) { - last_block = { type: 'text', children: [] }; - blocks.push(last_block); + let lastBlock: ClipBlockInfo = blocks[blocks.length - 1]; + if (!this._canEditText(lastBlock.type)) { + lastBlock = { type: 'text', children: [] }; + blocks.push(lastBlock); } - const last_values = last_block.properties?.text?.value; - last_text && last_values.push({ text: last_text }); - last_values.push(...last_cur_text_value); - last_block.properties = { - text: { value: last_values }, + const lastValues = lastBlock.properties?.text?.value; + lastText && lastValues.push({ text: lastText }); + lastValues.push(...lastCurTextValue); + lastBlock.properties = { + text: { value: lastValues }, }; - const insert_info = blocks[0].properties.text; - pre_text && pre_cur_text_value.push({ text: pre_text }); - pre_cur_text_value.push(...insert_info.value); - this.editor.blockHelper.setBlockBlur(cur_node_id); + 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 cur_block = await this.editor.getBlockById( - cur_node_id - ); - cur_block.setProperties({ - text: { value: pre_cur_text_value }, + const curBlock = await this._editor.getBlockById(curNodeId); + curBlock.setProperties({ + text: { value: pre_curTextValue }, }); - this.paste_children(cur_block, blocks[0].children); + await this._pasteChildren(curBlock, blocks[0].children); }, 0); - begin_index = 1; + beginIndex = 1; } } - for (let i = begin_index; i < blocks.length; i++) { - const next_block = await this.editor.createBlock(blocks[i].type); - next_block.setProperties(blocks[i].properties); - if (cur_block.type === 'page') { - cur_block.prepend(next_block); + 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 { - cur_block.after(next_block); + curBlock.after(nextBlock); } - this.paste_children(next_block, blocks[i].children); - cur_block = next_block; + await this._pasteChildren(nextBlock, blocks[i].children); + curBlock = nextBlock; } } - private async paste_children(parent: AsyncBlock, children: any[]) { + private async _pasteChildren(parent: AsyncBlock, children: any[]) { for (let i = 0; i < children.length; i++) { - const next_block = await this.editor.createBlock(children[i].type); - next_block.setProperties(children[i].properties); - parent.append(next_block); - await this.paste_children(next_block, children[i].children); + 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 pre_copy_cut(action: ClipboardAction, e: ClipboardEvent) { + private _preCopyCut(action: ClipboardAction, e: ClipboardEvent) { switch (action) { case ClipboardAction.COPY: - this.hooks.beforeCopy(e); + this._hooks.beforeCopy(e); break; case ClipboardAction.CUT: - this.hooks.beforeCut(e); + this._hooks.beforeCut(e); break; } } - private dispatch_clipboard_event( + private _dispatchClipboardEvent( action: ClipboardAction, e: ClipboardEvent ) { - this.pre_copy_cut(action, e); + this._preCopyCut(action, e); } dispose() { - document.removeEventListener(ClipboardAction.COPY, this.handle_copy); - document.removeEventListener(ClipboardAction.CUT, this.handle_cut); - document.removeEventListener(ClipboardAction.PASTE, this.handle_paste); - this.event_target.removeEventListener( + document.removeEventListener(ClipboardAction.COPY, this._handleCopy); + document.removeEventListener(ClipboardAction.CUT, this._handleCut); + document.removeEventListener(ClipboardAction.PASTE, this._handlePaste); + this._eventTarget.removeEventListener( ClipboardAction.COPY, - this.handle_copy + this._handleCopy ); - this.event_target.removeEventListener( + this._eventTarget.removeEventListener( ClipboardAction.CUT, - this.handle_cut + this._handleCut ); - this.event_target.removeEventListener( + this._eventTarget.removeEventListener( ClipboardAction.PASTE, - this.handle_paste + this._handlePaste ); - this.clipboard_parse.dispose(); - this.clipboard_parse = null; - this.event_target = null; - this.hooks = null; - this.editor = null; + this._clipboardParse.dispose(); + this._clipboardParse = null; + this._eventTarget = null; + this._hooks = null; + this._editor = null; } } From 36ad39237b20da14f35ec904b56b33821ebd6b74 Mon Sep 17 00:00:00 2001 From: DiamondThree Date: Tue, 9 Aug 2022 18:41:08 +0800 Subject: [PATCH 03/12] feat: add selection function --- .../src/editor/selection/selection.ts | 49 +++++++++++++------ 1 file changed, 34 insertions(+), 15 deletions(-) diff --git a/libs/components/editor-core/src/editor/selection/selection.ts b/libs/components/editor-core/src/editor/selection/selection.ts index b66d1b5fd..ad52fdd2d 100644 --- a/libs/components/editor-core/src/editor/selection/selection.ts +++ b/libs/components/editor-core/src/editor/selection/selection.ts @@ -30,6 +30,7 @@ import { } from './types'; import { isLikeBlockListIds } from './utils'; import { Protocol } from '@toeverything/datasource/db-service'; +import { Editor } from 'slate'; // IMP: maybe merge active and select into single function export type SelectionInfo = InstanceType< @@ -321,16 +322,16 @@ export class SelectionManager implements VirgoSelection { if (selectionRect.isIntersect(domToRect(block.dom))) { const childrenBlocks = await block.children(); // should check directly in structured block - const structuredChildrenBlocks: Array = - childrenBlocks.filter(childBlock => { + const structuredChildrenBlocks: Array = childrenBlocks.filter( + childBlock => { return this._editor.getView(childBlock.type).layoutOnly; - }); + } + ); for await (const childBlock of structuredChildrenBlocks) { - const childSelectedNodes = - await this.calcRenderBlockIntersect( - selectionRect, - childBlock - ); + const childSelectedNodes = await this.calcRenderBlockIntersect( + selectionRect, + childBlock + ); selectedNodes.push(...childSelectedNodes); } const selectableChildren = childrenBlocks.filter(childBlock => { @@ -347,11 +348,10 @@ export class SelectionManager implements VirgoSelection { } // if just only has one selected maybe select the children if (selectedNodes.length === 1) { - const childrenSelectedNodes: Array = - await this.calcRenderBlockIntersect( - selectionRect, - selectedNodes[0] - ); + const childrenSelectedNodes: Array = await this.calcRenderBlockIntersect( + selectionRect, + selectedNodes[0] + ); if (childrenSelectedNodes.length) return childrenSelectedNodes; } @@ -584,8 +584,7 @@ export class SelectionManager implements VirgoSelection { } else { const new_node = await this._editor.getBlockById(newNodeId); if (new_node) { - const new_node_children_ids = - await new_node.childrenIds; + const new_node_children_ids = await new_node.childrenIds; let select_ids_new = this._selectedNodesIds; if ( new_node_children_ids && @@ -1050,4 +1049,24 @@ export class SelectionManager implements VirgoSelection { this._windowSelectionChangeHandler ); } + /** + * + * move active selection to the new position + * @param index:number + * @memberof SelectionManager + */ + public moveCursor(index: number, blockId: string): void { + const now_range = window.getSelection().getRangeAt(0); + let preRang = document.createRange(); + preRang.setStart( + now_range.startContainer, + now_range.startOffset + index + ); + preRang.setEnd(now_range.endContainer, now_range.endOffset + index); + let prePosition = preRang.getClientRects().item(0); + this.activeNodeByNodeId( + blockId, + new Point(prePosition.left, prePosition.bottom) + ); + } } From 1b337e97667478daf36f363f8b26e8ac4a6adfef Mon Sep 17 00:00:00 2001 From: DiamondThree Date: Tue, 9 Aug 2022 18:43:09 +0800 Subject: [PATCH 04/12] feat: add selection function --- libs/components/editor-core/src/editor/selection/selection.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libs/components/editor-core/src/editor/selection/selection.ts b/libs/components/editor-core/src/editor/selection/selection.ts index ad52fdd2d..882d637b1 100644 --- a/libs/components/editor-core/src/editor/selection/selection.ts +++ b/libs/components/editor-core/src/editor/selection/selection.ts @@ -1052,7 +1052,8 @@ export class SelectionManager implements VirgoSelection { /** * * move active selection to the new position - * @param index:number + * @param {number} index + * @param {string} blockId * @memberof SelectionManager */ public moveCursor(index: number, blockId: string): void { From e795655bd94771696d41a2805219289d620bccc2 Mon Sep 17 00:00:00 2001 From: DiamondThree Date: Tue, 9 Aug 2022 18:57:41 +0800 Subject: [PATCH 05/12] fix:lint --- .../editor-core/src/editor/selection/selection.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/libs/components/editor-core/src/editor/selection/selection.ts b/libs/components/editor-core/src/editor/selection/selection.ts index 882d637b1..4b2da05cb 100644 --- a/libs/components/editor-core/src/editor/selection/selection.ts +++ b/libs/components/editor-core/src/editor/selection/selection.ts @@ -1057,13 +1057,10 @@ export class SelectionManager implements VirgoSelection { * @memberof SelectionManager */ public moveCursor(index: number, blockId: string): void { - const now_range = window.getSelection().getRangeAt(0); + const nowRange = window.getSelection().getRangeAt(0); let preRang = document.createRange(); - preRang.setStart( - now_range.startContainer, - now_range.startOffset + index - ); - preRang.setEnd(now_range.endContainer, now_range.endOffset + index); + preRang.setStart(nowRange.startContainer, nowRange.startOffset + index); + preRang.setEnd(nowRange.endContainer, nowRange.endOffset + index); let prePosition = preRang.getClientRects().item(0); this.activeNodeByNodeId( blockId, From 0f49444c059cdd06c3af9be8db6287dd7f4c34b8 Mon Sep 17 00:00:00 2001 From: QiShaoXuan Date: Wed, 10 Aug 2022 13:32:29 +0800 Subject: [PATCH 06/12] style: update code style --- .../editor/clipboard/clipboard-populator.ts | 47 +++++++++---------- 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/libs/components/editor-core/src/editor/clipboard/clipboard-populator.ts b/libs/components/editor-core/src/editor/clipboard/clipboard-populator.ts index 4d28ce3cf..2f05866ab 100644 --- a/libs/components/editor-core/src/editor/clipboard/clipboard-populator.ts +++ b/libs/components/editor-core/src/editor/clipboard/clipboard-populator.ts @@ -46,7 +46,7 @@ class ClipboardPopulator { } // TODO: is not compatible with safari - const success = this.copy_to_cliboard_from_pc(clips); + const success = this._copyToClipboardFromPc(clips); if (!success) { // This way, not compatible with firefox const clipboardData = e.clipboardData; @@ -65,7 +65,7 @@ class ClipboardPopulator { } }; - private copy_to_cliboard_from_pc(clips: any[]) { + private _copyToClipboardFromPc(clips: any[]) { let success = false; const tempElem = document.createElement('textarea'); tempElem.value = 'temp'; @@ -96,56 +96,55 @@ class ClipboardPopulator { return success; } - private async get_clip_block_info(selBlock: SelectBlock) { + private async _getClipBlockInfo(selBlock: SelectBlock) { const block = await this._editor.getBlockById(selBlock.blockId); - const block_view = this._editor.getView(block.type); - assert(block_view); - const block_info: ClipBlockInfo = { + const blockView = this._editor.getView(block.type); + assert(blockView); + const blockInfo: ClipBlockInfo = { type: block.type, - properties: block_view.getSelProperties(block, selBlock), + properties: blockView.getSelProperties(block, selBlock), children: [] as any[], }; for (let i = 0; i < selBlock.children.length; i++) { - const child_info = await this.get_clip_block_info( + const childInfo = await this._getClipBlockInfo( selBlock.children[i] ); - block_info.children.push(child_info); + blockInfo.children.push(childInfo); } - return block_info; + return blockInfo; } - private async get_inner_clip(): Promise { + private async _getInnerClip(): Promise { const clips: ClipBlockInfo[] = []; - const select_info: SelectInfo = + const selectInfo: SelectInfo = await this._selectionManager.getSelectInfo(); - for (let i = 0; i < select_info.blocks.length; i++) { - const sel_block = select_info.blocks[i]; - const clip_block_info = await this.get_clip_block_info(sel_block); - clips.push(clip_block_info); + for (let i = 0; i < selectInfo.blocks.length; i++) { + const selBlock = selectInfo.blocks[i]; + const clipBlockInfo = await this._getClipBlockInfo(selBlock); + clips.push(clipBlockInfo); } - const clipInfo: InnerClipInfo = { - select: select_info, + return { + select: selectInfo, data: clips, }; - return clipInfo; } async getClips() { const clips: any[] = []; - const inner_clip = await this.get_inner_clip(); + const innerClip = await this._getInnerClip(); clips.push( new Clip( OFFICE_CLIPBOARD_MIMETYPE.DOCS_DOCUMENT_SLICE_CLIP_WRAPPED, - JSON.stringify(inner_clip) + JSON.stringify(innerClip) ) ); - const html_clip = await this._clipboardParse.generateHtml(); - html_clip && - clips.push(new Clip(OFFICE_CLIPBOARD_MIMETYPE.HTML, html_clip)); + const htmlClip = await this._clipboardParse.generateHtml(); + htmlClip && + clips.push(new Clip(OFFICE_CLIPBOARD_MIMETYPE.HTML, htmlClip)); return clips; } From 9b0f608b6b2e713b5401f20983f1f60c83d40a98 Mon Sep 17 00:00:00 2001 From: DiamondThree Date: Thu, 11 Aug 2022 15:15:47 +0800 Subject: [PATCH 07/12] fix: add moveCursor function --- .../editor-core/src/editor/selection/selection.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/libs/components/editor-core/src/editor/selection/selection.ts b/libs/components/editor-core/src/editor/selection/selection.ts index 4b2da05cb..99f648a93 100644 --- a/libs/components/editor-core/src/editor/selection/selection.ts +++ b/libs/components/editor-core/src/editor/selection/selection.ts @@ -1056,11 +1056,14 @@ export class SelectionManager implements VirgoSelection { * @param {string} blockId * @memberof SelectionManager */ - public moveCursor(index: number, blockId: string): void { - const nowRange = window.getSelection().getRangeAt(0); + public async moveCursor( + nowRange: any, + index: number, + blockId: string + ): Promise { let preRang = document.createRange(); - preRang.setStart(nowRange.startContainer, nowRange.startOffset + index); - preRang.setEnd(nowRange.endContainer, nowRange.endOffset + index); + preRang.setStart(nowRange.startContainer, index); + preRang.setEnd(nowRange.endContainer, index); let prePosition = preRang.getClientRects().item(0); this.activeNodeByNodeId( blockId, From 9ad7332b30eb31889016b4d829ae37967842bae7 Mon Sep 17 00:00:00 2001 From: DiamondThree Date: Thu, 11 Aug 2022 15:39:22 +0800 Subject: [PATCH 08/12] fix: lint --- .../src/components/text-manage/TextManage.tsx | 124 ++++++++---------- 1 file changed, 57 insertions(+), 67 deletions(-) diff --git a/libs/components/editor-blocks/src/components/text-manage/TextManage.tsx b/libs/components/editor-blocks/src/components/text-manage/TextManage.tsx index 2e146c770..cc55e9b7b 100644 --- a/libs/components/editor-blocks/src/components/text-manage/TextManage.tsx +++ b/libs/components/editor-blocks/src/components/text-manage/TextManage.tsx @@ -42,13 +42,13 @@ const TextBlockContainer = styled(Text)(({ theme }) => ({ })); const findSlice = (arr: string[], p: string, q: string) => { - let should_include = false; + let shouldInclude = false; return arr.filter(block => { if (block === p || block === q) { - should_include = !should_include; + shouldInclude = !shouldInclude; return true; } else { - return should_include; + return shouldInclude; } }); }; @@ -112,11 +112,8 @@ export const TextManage = forwardRef( (ref as MutableRefObject) || defaultRef; const properties = block.getProperties(); - // const [is_select, set_is_select] = useState(); - // useOnSelect(block.id, (is_select: boolean) => { - // set_is_select(is_select); - // }); - const on_text_view_set_selection = (selection: Range | Point) => { + + const onTextViewSetSelection = (selection: Range | Point) => { if (selection instanceof Point) { //do some thing } else { @@ -125,18 +122,18 @@ export const TextManage = forwardRef( }; // block = await editor.commands.blockCommands.createNextBlock(block.id,) - const on_text_view_active = useCallback( + const onTextViewActive = useCallback( (point: CursorTypes) => { // TODO code to be optimized if (textRef.current) { - const end_selection = textRef.current.getEndSelection(); - const start_selection = textRef.current.getStartSelection(); + const endSelection = textRef.current.getEndSelection(); + const startSelection = textRef.current.getStartSelection(); if (point === 'start') { - textRef.current.setSelection(start_selection); + textRef.current.setSelection(startSelection); return; } if (point === 'end') { - textRef.current.setSelection(end_selection); + textRef.current.setSelection(endSelection); return; } try { @@ -151,24 +148,24 @@ export const TextManage = forwardRef( } else { blockTop = blockDomStyle.top + 5; } - const end_position = ReactEditor.toDOMRange( + const endPosition = ReactEditor.toDOMRange( textRef.current.editor, - end_selection + endSelection ) .getClientRects() .item(0); - const start_position = ReactEditor.toDOMRange( + const startPosition = ReactEditor.toDOMRange( textRef.current.editor, - start_selection + startSelection ) .getClientRects() .item(0); - if (end_position.left <= point.x) { - textRef.current.setSelection(end_selection); + if (endPosition.left <= point.x) { + textRef.current.setSelection(endSelection); return; } - if (start_position.left >= point.x) { - textRef.current.setSelection(start_selection); + if (startPosition.left >= point.x) { + textRef.current.setSelection(startSelection); return; } let range: globalThis.Range; @@ -186,7 +183,7 @@ export const TextManage = forwardRef( range = document.createRange(); range.setStart(caret.offsetNode, caret.offset); } - const slate_rang = ReactEditor.toSlateRange( + const slateRang = ReactEditor.toSlateRange( textRef.current.editor, range, { @@ -194,19 +191,19 @@ export const TextManage = forwardRef( suppressThrow: true, } ); - textRef.current.setSelection(slate_rang); + textRef.current.setSelection(slateRang); } } catch (e) { console.log('e: ', e); - textRef.current.setSelection(end_selection); + textRef.current.setSelection(endSelection); } } }, [textRef] ); - useOnSelectActive(block.id, on_text_view_active); - useOnSelectSetSelection<'Range'>(block.id, on_text_view_set_selection); + useOnSelectActive(block.id, onTextViewActive); + useOnSelectSetSelection<'Range'>(block.id, onTextViewSetSelection); useEffect(() => { if (textRef.current) { @@ -232,17 +229,17 @@ export const TextManage = forwardRef( (block.id === lastSelectNodeId && type === 'Range') || (type === 'Range' && info) ) { - on_text_view_active('end'); + onTextViewActive('end'); } else { - on_text_view_active('start'); + onTextViewActive('start'); } } } catch (e) { console.warn('error occured in set active in initialization'); } - }, [block.id, editor.selectionManager, on_text_view_active, textRef]); + }, [block.id, editor.selectionManager, onTextViewActive, textRef]); - const on_text_change: TextProps['handleChange'] = async ( + const onTextChange: TextProps['handleChange'] = async ( value, textStyle ) => { @@ -263,39 +260,34 @@ export const TextManage = forwardRef( }); } }; - const get_now_and_pre_rang_position = () => { - window.getSelection().getRangeAt(0); - // const now_range = - // editor.selectionManager.currentSelectInfo?.browserSelection.getRangeAt( - // 0 - // ); - const now_range = window.getSelection().getRangeAt(0); - let pre_position = null; - const now_position = now_range.getClientRects().item(0); + const getNowAndPreRangPosition = () => { + const nowRange = window.getSelection().getRangeAt(0); + let prePosition = null; + const nowPosition = nowRange.getClientRects().item(0); try { - if (now_range.startOffset !== 0) { - const pre_rang = document.createRange(); - pre_rang.setStart( - now_range.startContainer, - now_range.startOffset + 1 + if (nowRange.startOffset !== 0) { + const preRang = document.createRange(); + preRang.setStart( + nowRange.startContainer, + nowRange.startOffset + 1 ); - pre_rang.setEnd( - now_range.endContainer, - now_range.endOffset + 1 + preRang.setEnd( + nowRange.endContainer, + nowRange.endOffset + 1 ); - pre_position = pre_rang.getClientRects().item(0); + prePosition = preRang.getClientRects().item(0); } } catch (e) { // console.log(e); } - return { nowPosition: now_position, prePosition: pre_position }; + return { nowPosition: nowPosition, prePosition: prePosition }; }; const onKeyboardUp = (event: React.KeyboardEvent) => { // if default event is prevented do noting // if U want to disable up/down/enter use capture event for preventing if (!event.isDefaultPrevented()) { - const positions = get_now_and_pre_rang_position(); + const positions = getNowAndPreRangPosition(); const prePosition = positions.prePosition; const nowPosition = positions.nowPosition; if (prePosition) { @@ -336,7 +328,7 @@ export const TextManage = forwardRef( // editor.selectionManager.activeNextNode(block.id, 'start'); // return; if (!event.isDefaultPrevented()) { - const positions = get_now_and_pre_rang_position(); + const positions = getNowAndPreRangPosition(); const prePosition = positions.prePosition; const nowPosition = positions.nowPosition; // Create the last element range of slate_editor @@ -392,7 +384,7 @@ export const TextManage = forwardRef( return false; } }; - const on_select_all = () => { + const onSelectAll = () => { const isSelectAll = textRef.current.isEmpty() || textRef.current.isSelectAll(); if (isSelectAll) { @@ -402,22 +394,20 @@ export const TextManage = forwardRef( return false; }; - const on_undo = () => { + const onUndo = () => { editor.undo(); }; - const on_redo = () => { + const onRedo = () => { editor.redo(); }; - const on_keyboard_esc = () => { + const onKeyboardEsc = () => { if (editor.selectionManager.getSelectedNodesIds().length === 0) { - const active_node_id = + const activeNodeId = editor.selectionManager.getActivatedNodeId(); - if (active_node_id) { - editor.selectionManager.setSelectedNodesIds([ - active_node_id, - ]); + if (activeNodeId) { + editor.selectionManager.setSelectedNodesIds([activeNodeId]); ReactEditor.blur(textRef.current.editor); } } else { @@ -425,7 +415,7 @@ export const TextManage = forwardRef( } }; - const on_shift_click = async (e: MouseEvent) => { + const onShiftClick = async (e: MouseEvent) => { if (e.shiftKey) { const activeId = editor.selectionManager.getActivatedNodeId(); if (activeId === block.id) { @@ -474,16 +464,16 @@ export const TextManage = forwardRef( className={`${otherOptions.className}`} currentValue={properties.text.value} textStyle={properties.textStyle} - handleChange={on_text_change} + handleChange={onTextChange} handleUp={onKeyboardUp} handleDown={onKeyboardDown} handleLeft={onKeyboardLeft} handleRight={onKeyboardRight} - handleSelectAll={on_select_all} - handleMouseDown={on_shift_click} - handleUndo={on_undo} - handleRedo={on_redo} - handleEsc={on_keyboard_esc} + handleSelectAll={onSelectAll} + handleMouseDown={onShiftClick} + handleUndo={onUndo} + handleRedo={onRedo} + handleEsc={onKeyboardEsc} {...otherOptions} /> ); From 0a265f9981533cfa6e2055dbd247613ab3964553 Mon Sep 17 00:00:00 2001 From: QiShaoXuan Date: Thu, 11 Aug 2022 17:06:16 +0800 Subject: [PATCH 09/12] refactor: refactor paste behavior (not support selection) --- .../clipboard/browser-clipboard-back.ts | 422 ++++++++++++++ .../src/editor/clipboard/browser-clipboard.ts | 322 +---------- .../editor-core/src/editor/clipboard/paste.ts | 535 ++++++++++++++++++ .../editor-core/src/editor/clipboard/utils.ts | 14 + 4 files changed, 991 insertions(+), 302 deletions(-) create mode 100644 libs/components/editor-core/src/editor/clipboard/browser-clipboard-back.ts create mode 100644 libs/components/editor-core/src/editor/clipboard/paste.ts create mode 100644 libs/components/editor-core/src/editor/clipboard/utils.ts 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 000000000..6c6d4ad03 --- /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 6c6d4ad03..64eecbc5c 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 000000000..022b3de21 --- /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 000000000..7b4d420a3 --- /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'; +}; From 322fb098a4288ffcf94dcc22b0a7234d35cbeddc Mon Sep 17 00:00:00 2001 From: SaikaSakura Date: Thu, 11 Aug 2022 22:06:50 +0800 Subject: [PATCH 10/12] feat: repair paste cursor --- .../common/src/lib/text/slate-utils.ts | 13 ++++ .../src/editor/block/block-helper.ts | 66 ++++++++++++++++++- .../editor-core/src/editor/clipboard/paste.ts | 26 +++++--- .../editor-core/src/editor/clipboard/utils.ts | 2 +- .../src/editor/selection/selection.ts | 28 ++++---- 5 files changed, 109 insertions(+), 26 deletions(-) diff --git a/libs/components/common/src/lib/text/slate-utils.ts b/libs/components/common/src/lib/text/slate-utils.ts index 3f5fb5769..1783d79ca 100644 --- a/libs/components/common/src/lib/text/slate-utils.ts +++ b/libs/components/common/src/lib/text/slate-utils.ts @@ -905,6 +905,19 @@ class SlateUtils { ); } + public insertNodes( + nodes: SlateNode | Array, + options?: Parameters[2] + ) { + Transforms.insertNodes(this.editor, nodes, { + ...options, + }); + } + + public getNodeByPath(path: Path) { + Editor.node(this.editor, path); + } + public getStartSelection() { return { anchor: this.getStart(), diff --git a/libs/components/editor-core/src/editor/block/block-helper.ts b/libs/components/editor-core/src/editor/block/block-helper.ts index 1c1ab37db..7bd0284ff 100644 --- a/libs/components/editor-core/src/editor/block/block-helper.ts +++ b/libs/components/editor-core/src/editor/block/block-helper.ts @@ -3,7 +3,13 @@ import type { SlateUtils, TextAlignOptions, } from '@toeverything/components/common'; -import { Point, Selection as SlateSelection } from 'slate'; +import { + BaseRange, + Node, + Path, + Point, + Selection as SlateSelection, +} from 'slate'; import { Editor } from '../editor'; type TextUtilsFunctions = @@ -28,7 +34,10 @@ type TextUtilsFunctions = | 'removeSelection' | 'insertReference' | 'isCollapsed' - | 'blur'; + | 'blur' + | 'setSelection' + | 'insertNodes' + | 'getNodeByPath'; type ExtendedTextUtils = SlateUtils & { setLinkModalVisible: (visible: boolean) => void; @@ -193,6 +202,59 @@ export class BlockHelper { return ''; } + /** + * + * set selection of a text input + * @param {string} blockId + * @param {BaseRange} selection + * @return {*} + * @memberof BlockHelper + */ + public setSelection(blockId: string, selection: BaseRange) { + const text_utils = this._blockTextUtilsMap[blockId]; + if (text_utils) { + return text_utils.setSelection(selection); + } + console.warn('Could find the block text utils'); + } + + /** + * + * insert nodes in text + * @param {string} blockId + * @param {Array} nodes + * @param {Parameters[1]} options + * @return {*} + * @memberof BlockHelper + */ + public insertNodes( + blockId: string, + nodes: Array, + options?: Parameters[1] + ) { + const text_utils = this._blockTextUtilsMap[blockId]; + if (text_utils) { + return text_utils.insertNodes(nodes, options); + } + console.warn('Could find the block text utils'); + } + + /** + * + * get text(slate node) by path + * @param {string} blockId + * @param {Path} path + * @return {*} + * @memberof BlockHelper + */ + public getNodeByPath(blockId: string, path: Path) { + const text_utils = this._blockTextUtilsMap[blockId]; + if (text_utils) { + return text_utils.getNodeByPath(path); + } + console.warn('Could find the block text utils'); + } + public transformPoint( blockId: string, ...restArgs: Parameters diff --git a/libs/components/editor-core/src/editor/clipboard/paste.ts b/libs/components/editor-core/src/editor/clipboard/paste.ts index 022b3de21..6633b618e 100644 --- a/libs/components/editor-core/src/editor/clipboard/paste.ts +++ b/libs/components/editor-core/src/editor/clipboard/paste.ts @@ -1,4 +1,4 @@ -import { HooksRunner } from '../types'; +/* eslint-disable max-lines */ import { OFFICE_CLIPBOARD_MIMETYPE, InnerClipInfo, @@ -187,10 +187,15 @@ export class Paste { const pureText = !shouldSplitBlock ? blocks[0].properties.text.value : [{ text: '' }]; - this._editor.blockHelper.setBlockBlur( - currentSelectInfo.blocks[0].blockId + this._editor.blockHelper.insertNodes( + selectedBlock.id, + pureText, + { select: true } ); + return; + //TODO repair the following logics + /** const { startInfo, endInfo } = currentSelectInfo.blocks[0]; // 选中的当前的可编辑block的文字信息 @@ -414,13 +419,13 @@ export class Paste { }, }); - const pastedTextLength = pureText.reduce( - (sumLength: number, textItem: TextValueItem) => { - sumLength += textItem.text.length; - return sumLength; - }, - 0 - ); + // const pastedTextLength = pureText.reduce( + // (sumLength: number, textItem: TextValueItem) => { + // sumLength += textItem.text.length; + // return sumLength; + // }, + // 0 + // ); // this._editor.selectionManager.moveCursor( // window.getSelection().getRangeAt(0), @@ -429,6 +434,7 @@ export class Paste { // ); } } + */ } else { const pasteBlocks = await this._createBlocks(blocks); pasteBlocks.forEach(block => { diff --git a/libs/components/editor-core/src/editor/clipboard/utils.ts b/libs/components/editor-core/src/editor/clipboard/utils.ts index 7b4d420a3..cb5d10241 100644 --- a/libs/components/editor-core/src/editor/clipboard/utils.ts +++ b/libs/components/editor-core/src/editor/clipboard/utils.ts @@ -1,4 +1,4 @@ -import {Editor} from "../editor"; +import { Editor } from '../editor'; export const shouldHandlerContinue = (event: Event, editor: Editor) => { const filterNodes = ['INPUT', 'SELECT', 'TEXTAREA']; diff --git a/libs/components/editor-core/src/editor/selection/selection.ts b/libs/components/editor-core/src/editor/selection/selection.ts index 99f648a93..40c069b94 100644 --- a/libs/components/editor-core/src/editor/selection/selection.ts +++ b/libs/components/editor-core/src/editor/selection/selection.ts @@ -322,16 +322,16 @@ export class SelectionManager implements VirgoSelection { if (selectionRect.isIntersect(domToRect(block.dom))) { const childrenBlocks = await block.children(); // should check directly in structured block - const structuredChildrenBlocks: Array = childrenBlocks.filter( - childBlock => { + const structuredChildrenBlocks: Array = + childrenBlocks.filter(childBlock => { return this._editor.getView(childBlock.type).layoutOnly; - } - ); + }); for await (const childBlock of structuredChildrenBlocks) { - const childSelectedNodes = await this.calcRenderBlockIntersect( - selectionRect, - childBlock - ); + const childSelectedNodes = + await this.calcRenderBlockIntersect( + selectionRect, + childBlock + ); selectedNodes.push(...childSelectedNodes); } const selectableChildren = childrenBlocks.filter(childBlock => { @@ -348,10 +348,11 @@ export class SelectionManager implements VirgoSelection { } // if just only has one selected maybe select the children if (selectedNodes.length === 1) { - const childrenSelectedNodes: Array = await this.calcRenderBlockIntersect( - selectionRect, - selectedNodes[0] - ); + const childrenSelectedNodes: Array = + await this.calcRenderBlockIntersect( + selectionRect, + selectedNodes[0] + ); if (childrenSelectedNodes.length) return childrenSelectedNodes; } @@ -584,7 +585,8 @@ export class SelectionManager implements VirgoSelection { } else { const new_node = await this._editor.getBlockById(newNodeId); if (new_node) { - const new_node_children_ids = await new_node.childrenIds; + const new_node_children_ids = + await new_node.childrenIds; let select_ids_new = this._selectedNodesIds; if ( new_node_children_ids && From 6ad94242c4a0117735e122a11f2502f4883f1902 Mon Sep 17 00:00:00 2001 From: QiShaoXuan Date: Thu, 11 Aug 2022 22:33:51 +0800 Subject: [PATCH 11/12] fix: insert correct selection after paste --- .../editor-core/src/editor/clipboard/paste.ts | 116 +++--------------- 1 file changed, 15 insertions(+), 101 deletions(-) diff --git a/libs/components/editor-core/src/editor/clipboard/paste.ts b/libs/components/editor-core/src/editor/clipboard/paste.ts index 6633b618e..0c48eee56 100644 --- a/libs/components/editor-core/src/editor/clipboard/paste.ts +++ b/libs/components/editor-core/src/editor/clipboard/paste.ts @@ -170,8 +170,8 @@ export class Paste { const currentSelectInfo = await this._editor.selectionManager.getSelectInfo(); - // 当选区在某一个block中时 - // select?.type === 'Range' + // When the selection is in one of the blocks, select?.type === 'Range' + // Currently the selection does not support cross-blocking, so this case is not considered if (currentSelectInfo.type === 'Range') { // 当 currentSelectInfo.type === 'Range' 时,光标选中的block必然只有一个 const selectedBlock = await this._editor.getBlockById( @@ -187,23 +187,15 @@ export class Paste { const pureText = !shouldSplitBlock ? blocks[0].properties.text.value : [{ text: '' }]; - this._editor.blockHelper.insertNodes( - selectedBlock.id, - pureText, - { select: true } - ); - return; - //TODO repair the following logics - - /** const { startInfo, endInfo } = currentSelectInfo.blocks[0]; - // 选中的当前的可编辑block的文字信息 + // Text content of the selected current editable block const currentTextValue = selectedBlock.getProperty('text').value; - // 当光标选区跨越不同样式文字时 + // When the cursor selection spans different styles of text if (startInfo?.arrayIndex !== endInfo?.arrayIndex) { if (shouldSplitBlock) { + // TODO: split block maybe should use slate method to support, like "this._editor.blockHelper.insertNodes" const newTextValue = currentTextValue.reduce( ( newTextValue: TextValueItem[], @@ -270,50 +262,17 @@ export class Paste { 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; - }, - [] + this._editor.blockHelper.insertNodes( + selectedBlock.id, + pureText, + { select: true } ); - newTextValue.splice( - startInfo?.arrayIndex + 1, - 0, - ...pureText - ); - selectedBlock.setProperties({ - text: { - value: newTextValue, - }, - }); } } - // 当光标选区没有跨越不同样式文字时 + // When the cursor selection does not span different styles of text if (startInfo?.arrayIndex === endInfo?.arrayIndex) { if (shouldSplitBlock) { + // TODO: split block maybe should use slate method to support, like "this._editor.blockHelper.insertNodes" const newTextValue = currentTextValue.reduce( ( newTextValue: TextValueItem[], @@ -383,58 +342,13 @@ export class Paste { 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; - }, - [] + this._editor.blockHelper.insertNodes( + selectedBlock.id, + pureText, + { select: true } ); - 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 => { From 0674a4885455ce08f5bfa28576a645580c9ffad1 Mon Sep 17 00:00:00 2001 From: QiShaoXuan Date: Thu, 11 Aug 2022 22:49:42 +0800 Subject: [PATCH 12/12] refactor: remove useless file --- .../clipboard/browser-clipboard-back.ts | 422 ------------------ 1 file changed, 422 deletions(-) delete mode 100644 libs/components/editor-core/src/editor/clipboard/browser-clipboard-back.ts 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 deleted file mode 100644 index 6c6d4ad03..000000000 --- a/libs/components/editor-core/src/editor/clipboard/browser-clipboard-back.ts +++ /dev/null @@ -1,422 +0,0 @@ -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 };