From 17e454b1e65026cdd804602d2fb076f6b4f1013e Mon Sep 17 00:00:00 2001 From: QiShaoXuan Date: Sun, 21 Aug 2022 22:58:41 +0800 Subject: [PATCH] refactor: refactor clipboard --- .../common/src/lib/text/slate-utils.ts | 37 ++++- .../editor-blocks/src/blocks/bullet/index.ts | 9 -- .../editor-blocks/src/blocks/code/index.ts | 9 -- .../src/blocks/numbered/index.ts | 9 -- .../editor-blocks/src/blocks/page/index.ts | 9 -- .../src/blocks/text/QuoteBlock.tsx | 17 --- .../src/blocks/text/TextBlock.tsx | 33 ----- .../editor-blocks/src/blocks/todo/index.ts | 9 -- .../editor-blocks/src/blocks/youtube/index.ts | 4 +- .../src/editor/block/block-helper.ts | 114 +++++++++++++++- .../src/editor/clipboard/browser-clipboard.ts | 128 ----------------- .../editor/clipboard/clipboard-populator.ts | 38 ------ .../src/editor/clipboard/clipboard.ts | 47 +++++++ .../clipboard/clipboardEventDispatcher.ts | 82 +++++++++++ .../src/editor/clipboard/clipboardUtils.ts | 121 ++++++++++++++++ .../editor-core/src/editor/clipboard/copy.ts | 129 +++++++++++++++++- .../editor-core/src/editor/clipboard/index.ts | 0 .../editor-core/src/editor/clipboard/paste.ts | 4 +- .../editor-core/src/editor/clipboard/utils.ts | 8 +- .../editor-core/src/editor/editor.ts | 63 ++++----- .../editor-core/src/editor/index.ts | 2 +- .../editor-core/src/editor/plugin/hooks.ts | 11 +- .../editor-core/src/editor/types.ts | 10 +- .../editor-core/src/editor/views/base-view.ts | 45 +++--- 24 files changed, 592 insertions(+), 346 deletions(-) delete mode 100644 libs/components/editor-core/src/editor/clipboard/browser-clipboard.ts delete mode 100644 libs/components/editor-core/src/editor/clipboard/clipboard-populator.ts create mode 100644 libs/components/editor-core/src/editor/clipboard/clipboard.ts create mode 100644 libs/components/editor-core/src/editor/clipboard/clipboardEventDispatcher.ts create mode 100644 libs/components/editor-core/src/editor/clipboard/clipboardUtils.ts create mode 100644 libs/components/editor-core/src/editor/clipboard/index.ts diff --git a/libs/components/common/src/lib/text/slate-utils.ts b/libs/components/common/src/lib/text/slate-utils.ts index ec7e219810..fb183a7c35 100644 --- a/libs/components/common/src/lib/text/slate-utils.ts +++ b/libs/components/common/src/lib/text/slate-utils.ts @@ -24,6 +24,7 @@ import { MARKDOWN_STYLE_MAP, MatchRes, } from './utils'; +import { AsyncBlock, SelectBlock } from '@toeverything/components/editor-core'; function isInlineAndVoid(editor: Editor, el: any) { return editor.isInline(el) && editor.isVoid(el); @@ -958,7 +959,41 @@ class SlateUtils { } public getNodeByPath(path: Path) { - Editor.node(this.editor, path); + return Editor.node(this.editor, path); + } + + public getNodeByRange(range: Range) { + return Editor.node(this.editor, range); + } + + // This may should write with logic of render slate + public convertLeaf2Html(textValue: any) { + const { text, fontColor, fontBgColor } = textValue; + + const style = `style="${fontColor ? `color: ${fontColor};` : ''}${ + fontBgColor ? `backgroundColor: ${fontBgColor};` : '' + }"`; + if (textValue.bold) { + return `${text}`; + } + if (textValue.italic) { + return `${text}`; + } + if (textValue.underline) { + return `${text}`; + } + if (textValue.inlinecode) { + return `${text}`; + } + if (textValue.strikethrough) { + return `${text}`; + } + if (textValue.type === 'link') { + return `${this.convertLeaf2Html( + textValue.children + )}`; + } + return `${text}>`; } public getStartSelection() { diff --git a/libs/components/editor-blocks/src/blocks/bullet/index.ts b/libs/components/editor-blocks/src/blocks/bullet/index.ts index 087afb3056..58360954ce 100644 --- a/libs/components/editor-blocks/src/blocks/bullet/index.ts +++ b/libs/components/editor-blocks/src/blocks/bullet/index.ts @@ -2,7 +2,6 @@ import { AsyncBlock, BaseView, CreateView, - getTextProperties, SelectBlock, getTextHtml, } from '@toeverything/framework/virgo'; @@ -28,14 +27,6 @@ export class BulletBlock extends BaseView { return block; } - override getSelProperties( - block: AsyncBlock, - selectInfo: any - ): DefaultColumnsValue { - const properties = super.getSelProperties(block, selectInfo); - return getTextProperties(properties, selectInfo); - } - override html2block( el: Element, parseEl: (el: Element) => any[] diff --git a/libs/components/editor-blocks/src/blocks/code/index.ts b/libs/components/editor-blocks/src/blocks/code/index.ts index 9b089d6db5..1fc84f1df8 100644 --- a/libs/components/editor-blocks/src/blocks/code/index.ts +++ b/libs/components/editor-blocks/src/blocks/code/index.ts @@ -1,7 +1,6 @@ import { BaseView, AsyncBlock, - getTextProperties, CreateView, SelectBlock, getTextHtml, @@ -28,14 +27,6 @@ export class CodeBlock extends BaseView { return block; } - override getSelProperties( - block: AsyncBlock, - selectInfo: any - ): DefaultColumnsValue { - const properties = super.getSelProperties(block, selectInfo); - return getTextProperties(properties, selectInfo); - } - // TODO: internal format not implemented yet override html2block( el: Element, diff --git a/libs/components/editor-blocks/src/blocks/numbered/index.ts b/libs/components/editor-blocks/src/blocks/numbered/index.ts index b8b3c3dc0a..37295e026f 100644 --- a/libs/components/editor-blocks/src/blocks/numbered/index.ts +++ b/libs/components/editor-blocks/src/blocks/numbered/index.ts @@ -1,7 +1,6 @@ import { AsyncBlock, BaseView, - getTextProperties, SelectBlock, getTextHtml, } from '@toeverything/framework/virgo'; @@ -29,14 +28,6 @@ export class NumberedBlock extends BaseView { return block; } - override getSelProperties( - block: AsyncBlock, - selectInfo: any - ): DefaultColumnsValue { - const properties = super.getSelProperties(block, selectInfo); - return getTextProperties(properties, selectInfo); - } - override html2block( el: Element, parseEl: (el: Element) => any[] diff --git a/libs/components/editor-blocks/src/blocks/page/index.ts b/libs/components/editor-blocks/src/blocks/page/index.ts index 412bbdbd7c..06151bc481 100644 --- a/libs/components/editor-blocks/src/blocks/page/index.ts +++ b/libs/components/editor-blocks/src/blocks/page/index.ts @@ -8,7 +8,6 @@ import { BaseView, ChildrenView, getTextHtml, - getTextProperties, SelectBlock, } from '@toeverything/framework/virgo'; @@ -35,14 +34,6 @@ export class PageBlock extends BaseView { return this.get_decoration(content, 'text')?.value?.[0].text; } - override getSelProperties( - block: AsyncBlock, - selectInfo: any - ): DefaultColumnsValue { - const properties = super.getSelProperties(block, selectInfo); - return getTextProperties(properties, selectInfo); - } - override async block2html( block: AsyncBlock, children: SelectBlock[], diff --git a/libs/components/editor-blocks/src/blocks/text/QuoteBlock.tsx b/libs/components/editor-blocks/src/blocks/text/QuoteBlock.tsx index f62dfae040..94938aa595 100644 --- a/libs/components/editor-blocks/src/blocks/text/QuoteBlock.tsx +++ b/libs/components/editor-blocks/src/blocks/text/QuoteBlock.tsx @@ -7,7 +7,6 @@ import { BaseView, CreateView, getTextHtml, - getTextProperties, SelectBlock, } from '@toeverything/framework/virgo'; @@ -29,14 +28,6 @@ export class QuoteBlock extends BaseView { return block; } - override getSelProperties( - block: AsyncBlock, - selectInfo: any - ): DefaultColumnsValue { - const properties = super.getSelProperties(block, selectInfo); - return getTextProperties(properties, selectInfo); - } - override html2block( el: Element, parseEl: (el: Element) => any[] @@ -96,14 +87,6 @@ export class CalloutBlock extends BaseView { return block; } - override getSelProperties( - block: AsyncBlock, - selectInfo: any - ): DefaultColumnsValue { - const properties = super.getSelProperties(block, selectInfo); - return getTextProperties(properties, selectInfo); - } - override html2block( el: Element, parseEl: (el: Element) => any[] diff --git a/libs/components/editor-blocks/src/blocks/text/TextBlock.tsx b/libs/components/editor-blocks/src/blocks/text/TextBlock.tsx index 64902e252d..74085db922 100644 --- a/libs/components/editor-blocks/src/blocks/text/TextBlock.tsx +++ b/libs/components/editor-blocks/src/blocks/text/TextBlock.tsx @@ -2,7 +2,6 @@ import { BaseView, CreateView, AsyncBlock, - getTextProperties, SelectBlock, getTextHtml, } from '@toeverything/framework/virgo'; @@ -28,14 +27,6 @@ export class TextBlock extends BaseView { return block; } - override getSelProperties( - block: AsyncBlock, - selectInfo: any - ): DefaultColumnsValue { - const properties = super.getSelProperties(block, selectInfo); - return getTextProperties(properties, selectInfo); - } - override html2block( el: Element, parseEl: (el: Element) => any[] @@ -145,14 +136,6 @@ export class Heading1Block extends BaseView { return block; } - override getSelProperties( - block: AsyncBlock, - selectInfo: any - ): DefaultColumnsValue { - const properties = super.getSelProperties(block, selectInfo); - return getTextProperties(properties, selectInfo); - } - override html2block( el: Element, parseEl: (el: Element) => any[] @@ -210,14 +193,6 @@ export class Heading2Block extends BaseView { return block; } - override getSelProperties( - block: AsyncBlock, - selectInfo: any - ): DefaultColumnsValue { - const properties = super.getSelProperties(block, selectInfo); - return getTextProperties(properties, selectInfo); - } - override html2block( el: Element, parseEl: (el: Element) => any[] @@ -275,14 +250,6 @@ export class Heading3Block extends BaseView { return block; } - override getSelProperties( - block: AsyncBlock, - selectInfo: any - ): DefaultColumnsValue { - const properties = super.getSelProperties(block, selectInfo); - return getTextProperties(properties, selectInfo); - } - override html2block( el: Element, parseEl: (el: Element) => any[] diff --git a/libs/components/editor-blocks/src/blocks/todo/index.ts b/libs/components/editor-blocks/src/blocks/todo/index.ts index c6580009d0..db4187a890 100644 --- a/libs/components/editor-blocks/src/blocks/todo/index.ts +++ b/libs/components/editor-blocks/src/blocks/todo/index.ts @@ -1,6 +1,5 @@ import { BaseView, - getTextProperties, AsyncBlock, SelectBlock, getTextHtml, @@ -29,14 +28,6 @@ export class TodoBlock extends BaseView { return block; } - override getSelProperties( - block: TodoAsyncBlock, - selectInfo: any - ): DefaultColumnsValue { - const properties = super.getSelProperties(block, selectInfo); - return getTextProperties(properties, selectInfo); - } - override html2block( el: Element, parseEl: (el: Element) => any[] diff --git a/libs/components/editor-blocks/src/blocks/youtube/index.ts b/libs/components/editor-blocks/src/blocks/youtube/index.ts index 42f22bcda4..94ad3ac59d 100644 --- a/libs/components/editor-blocks/src/blocks/youtube/index.ts +++ b/libs/components/editor-blocks/src/blocks/youtube/index.ts @@ -41,7 +41,9 @@ export class YoutubeBlock extends BaseView { return null; } - + override async block2Text(block: AsyncBlock, selectInfo: SelectBlock) { + return block.getProperty('embedLink')?.value ?? ''; + } override async block2html( block: AsyncBlock, children: SelectBlock[], 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 7bd0284ffc..70238c4be5 100644 --- a/libs/components/editor-core/src/editor/block/block-helper.ts +++ b/libs/components/editor-core/src/editor/block/block-helper.ts @@ -11,6 +11,11 @@ import { Selection as SlateSelection, } from 'slate'; import { Editor } from '../editor'; +import { + AsyncBlock, + SelectBlock, + SelectInfo, +} from '@toeverything/components/editor-core'; type TextUtilsFunctions = | 'getString' @@ -37,7 +42,9 @@ type TextUtilsFunctions = | 'blur' | 'setSelection' | 'insertNodes' - | 'getNodeByPath'; + | 'getNodeByPath' + | 'getNodeByRange' + | 'convertLeaf2Html'; type ExtendedTextUtils = SlateUtils & { setLinkModalVisible: (visible: boolean) => void; @@ -98,15 +105,116 @@ export class BlockHelper { return ''; } - public getBlockTextBetweenSelection(blockId: string) { + public async isBlockEditable(blockOrBlockId: AsyncBlock | string) { + const block = + typeof blockOrBlockId === 'string' + ? await this._editor.getBlockById(blockOrBlockId) + : blockOrBlockId; + const blockView = this._editor.getView(block.type); + + return blockView.activatable; + } + + public async getFlatBlocksUnderParent( + parentBlockId: string, + includeParent: boolean = false + ): Promise { + const blocks = []; + const block = await this._editor.getBlockById(parentBlockId); + if (includeParent) { + blocks.push(block); + } + const children = await block.children(); + ( + await Promise.all( + children.map(child => { + return this.getFlatBlocksUnderParent(child.id, true); + }) + ) + ).forEach(editableChildren => { + blocks.push(...editableChildren); + }); + return blocks; + } + + public getBlockTextBetweenSelection( + blockId: string, + shouldUsePreviousSelection = true + ) { const text_utils = this._blockTextUtilsMap[blockId]; if (text_utils) { - return text_utils.getStringBetweenSelection(true); + return text_utils.getStringBetweenSelection( + shouldUsePreviousSelection + ); } console.warn('Could find the block text utils'); return ''; } + public async getEditableBlockPropertiesBySelectInfo( + block: AsyncBlock, + selectInfo: SelectBlock + ) { + const properties = block.getProperties(); + if (properties.text.value.length === 0) { + return properties; + } + let text_value = properties.text.value; + + const { + text: { value: originTextValue, ...otherTextProperties }, + ...otherProperties + } = properties; + + // Use deepClone method will throw incomprehensible error + let textValue = JSON.parse(JSON.stringify(originTextValue)); + + if (selectInfo.endInfo) { + textValue = textValue.slice(0, selectInfo.endInfo.arrayIndex + 1); + textValue[textValue.length - 1].text = text_value[ + textValue.length - 1 + ].text.substring(0, selectInfo.endInfo.offset); + } + if (selectInfo.startInfo) { + textValue = textValue.slice(selectInfo.startInfo.arrayIndex); + textValue[0].text = textValue[0].text.substring( + selectInfo.startInfo.offset + ); + } + return { + ...otherProperties, + text: { + ...otherTextProperties, + value: textValue, + }, + }; + } + + // For editable blocks, the properties containing the selected text will be returned with the selection information + public async getBlockPropertiesBySelectInfo(selectBlockInfo: SelectBlock) { + const block = await this._editor.getBlockById(selectBlockInfo.blockId); + const blockView = this._editor.getView(block.type); + if (blockView.activatable) { + return this.getEditableBlockPropertiesBySelectInfo( + block, + selectBlockInfo + ); + } else { + return block?.getProperties(); + } + } + + public convertTextValue2Html(blockId: string, textValue: any) { + const text_utils = this._blockTextUtilsMap[blockId]; + if (!text_utils) { + return ''; + } + return textValue.reduce((html: string, textValueItem: any) => { + const fragment = text_utils.convertLeaf2Html(textValueItem); + return `${html}${fragment}`; + }, ''); + } + public setBlockBlur(blockId: string) { const text_utils = this._blockTextUtilsMap[blockId]; if (text_utils) { diff --git a/libs/components/editor-core/src/editor/clipboard/browser-clipboard.ts b/libs/components/editor-core/src/editor/clipboard/browser-clipboard.ts deleted file mode 100644 index 687c5280ee..0000000000 --- a/libs/components/editor-core/src/editor/clipboard/browser-clipboard.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { HooksRunner } from '../types'; -import { Editor } from '../editor'; -import ClipboardParse from './clipboard-parse'; -import { MarkdownParser } from './markdown-parse'; -import { shouldHandlerContinue } from './utils'; -import { Paste } from './paste'; -// todo needs to be a switch - -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 _paste: Paste; - - 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._paste = new Paste( - editor, - this._clipboardParse, - this._markdownParse - ); - this._initialize(); - } - - public getClipboardParse() { - return this._clipboardParse; - } - - private _initialize() { - this._handleCopy = this._handleCopy.bind(this); - this._handleCut = this._handleCut.bind(this); - - document.addEventListener(ClipboardAction.COPY, this._handleCopy); - document.addEventListener(ClipboardAction.CUT, this._handleCut); - document.addEventListener( - ClipboardAction.PASTE, - this._paste.handlePaste - ); - this._eventTarget.addEventListener( - ClipboardAction.COPY, - this._handleCopy - ); - this._eventTarget.addEventListener( - ClipboardAction.CUT, - this._handleCut - ); - this._eventTarget.addEventListener( - ClipboardAction.PASTE, - this._paste.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 _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._paste.handlePaste - ); - this._eventTarget.removeEventListener( - ClipboardAction.COPY, - this._handleCopy - ); - this._eventTarget.removeEventListener( - ClipboardAction.CUT, - this._handleCut - ); - this._eventTarget.removeEventListener( - ClipboardAction.PASTE, - this._paste.handlePaste - ); - this._clipboardParse.dispose(); - this._clipboardParse = null; - this._eventTarget = null; - this._hooks = null; - this._editor = null; - } -} - -export { BrowserClipboard }; diff --git a/libs/components/editor-core/src/editor/clipboard/clipboard-populator.ts b/libs/components/editor-core/src/editor/clipboard/clipboard-populator.ts deleted file mode 100644 index d7a8172566..0000000000 --- a/libs/components/editor-core/src/editor/clipboard/clipboard-populator.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { Editor } from '../editor'; -import { SelectionManager } from '../selection'; -import { HookType, PluginHooks } from '../types'; -import ClipboardParse from './clipboard-parse'; -import { Subscription } from 'rxjs'; -import { Copy } from './copy'; -class ClipboardPopulator { - private _editor: Editor; - private _hooks: PluginHooks; - private _selectionManager: SelectionManager; - private _clipboardParse: ClipboardParse; - private _sub = new Subscription(); - private _copy: Copy; - constructor( - editor: Editor, - hooks: PluginHooks, - selectionManager: SelectionManager - ) { - this._editor = editor; - this._hooks = hooks; - this._selectionManager = selectionManager; - this._clipboardParse = new ClipboardParse(editor); - this._copy = new Copy(editor); - this._sub.add( - hooks.get(HookType.BEFORE_COPY).subscribe(this._copy.handleCopy) - ); - this._sub.add( - hooks.get(HookType.BEFORE_CUT).subscribe(this._copy.handleCopy) - ); - } - - disposeInternal() { - this._sub.unsubscribe(); - this._hooks = null; - } -} - -export { ClipboardPopulator }; diff --git a/libs/components/editor-core/src/editor/clipboard/clipboard.ts b/libs/components/editor-core/src/editor/clipboard/clipboard.ts new file mode 100644 index 0000000000..4891f7dffc --- /dev/null +++ b/libs/components/editor-core/src/editor/clipboard/clipboard.ts @@ -0,0 +1,47 @@ +import { ClipboardEventDispatcher } from './clipboardEventDispatcher'; +import { HookType } from '@toeverything/components/editor-core'; +import { Editor } from '../editor'; +import { Copy } from './copy'; +import { Paste } from './paste'; +import ClipboardParse from './clipboard-parse'; +import { MarkdownParser } from './markdown-parse'; + +export class Clipboard { + private _clipboardEventDispatcher: ClipboardEventDispatcher; + private _copy: Copy; + private _paste: Paste; + private _clipboardParse: ClipboardParse; + private _markdownParse: MarkdownParser; + + constructor(editor: Editor, clipboardTarget: HTMLElement) { + this._clipboardEventDispatcher = new ClipboardEventDispatcher( + editor, + clipboardTarget + ); + this._clipboardParse = new ClipboardParse(editor); + this._markdownParse = new MarkdownParser(); + this._copy = new Copy(editor); + + this._paste = new Paste( + editor, + this._clipboardParse, + this._markdownParse + ); + + editor + .getHooks() + .get(HookType.ON_COPY) + .subscribe(this._copy.handleCopy); + + editor.getHooks().get(HookType.ON_CUT).subscribe(this._copy.handleCopy); + + editor + .getHooks() + .get(HookType.ON_PASTE) + .subscribe(this._paste.handlePaste); + } + + public dispose() { + this._clipboardEventDispatcher.dispose(); + } +} diff --git a/libs/components/editor-core/src/editor/clipboard/clipboardEventDispatcher.ts b/libs/components/editor-core/src/editor/clipboard/clipboardEventDispatcher.ts new file mode 100644 index 0000000000..d529e20a84 --- /dev/null +++ b/libs/components/editor-core/src/editor/clipboard/clipboardEventDispatcher.ts @@ -0,0 +1,82 @@ +import { Editor } from '../editor'; +import { shouldHandlerContinue } from './utils'; + +enum ClipboardAction { + copy = 'copy', + cut = 'cut', + paste = 'paste', +} + +//TODO: need to consider the cursor position after inserting the children +export class ClipboardEventDispatcher { + private _editor: Editor; + private _clipboardTarget: HTMLElement; + + constructor(editor: Editor, clipboardTarget: HTMLElement) { + this._editor = editor; + this._clipboardTarget = clipboardTarget; + this._initialize(); + } + + private _initialize() { + this._copyHandler = this._copyHandler.bind(this); + this._cutHandler = this._cutHandler.bind(this); + this._pasteHandler = this._pasteHandler.bind(this); + + document.addEventListener(ClipboardAction.copy, this._copyHandler); + document.addEventListener(ClipboardAction.cut, this._cutHandler); + document.addEventListener(ClipboardAction.paste, this._pasteHandler); + this._clipboardTarget.addEventListener( + ClipboardAction.copy, + this._copyHandler + ); + this._clipboardTarget.addEventListener( + ClipboardAction.cut, + this._cutHandler + ); + this._clipboardTarget.addEventListener( + ClipboardAction.paste, + this._pasteHandler + ); + } + + private _copyHandler(e: ClipboardEvent) { + if (!shouldHandlerContinue(e, this._editor)) { + return; + } + this._editor.getHooks().onCopy(e); + } + + private _cutHandler(e: ClipboardEvent) { + if (!shouldHandlerContinue(e, this._editor)) { + return; + } + this._editor.getHooks().onCut(e); + } + private _pasteHandler(e: ClipboardEvent) { + if (!shouldHandlerContinue(e, this._editor)) { + return; + } + + this._editor.getHooks().onPaste(e); + } + + dispose() { + document.removeEventListener(ClipboardAction.copy, this._copyHandler); + document.removeEventListener(ClipboardAction.cut, this._cutHandler); + document.removeEventListener(ClipboardAction.paste, this._pasteHandler); + this._clipboardTarget.removeEventListener( + ClipboardAction.copy, + this._copyHandler + ); + this._clipboardTarget.removeEventListener( + ClipboardAction.cut, + this._cutHandler + ); + this._clipboardTarget.removeEventListener( + ClipboardAction.paste, + this._pasteHandler + ); + this._editor = null; + } +} diff --git a/libs/components/editor-core/src/editor/clipboard/clipboardUtils.ts b/libs/components/editor-core/src/editor/clipboard/clipboardUtils.ts new file mode 100644 index 0000000000..1a31620fb6 --- /dev/null +++ b/libs/components/editor-core/src/editor/clipboard/clipboardUtils.ts @@ -0,0 +1,121 @@ +import { Editor } from '../editor'; +import { + AsyncBlock, + SelectBlock, + SelectInfo, +} from '@toeverything/components/editor-core'; +import { ClipBlockInfo, OFFICE_CLIPBOARD_MIMETYPE } from './types'; +import { getClipInfoOfBlockById } from './utils'; +import { Clip } from './clip'; + +export class ClipboardUtils { + private _editor: Editor; + constructor(editor: Editor) { + this._editor = editor; + } + + async isBlockEditable(blockOrBlockId: AsyncBlock | string) { + const block = + typeof blockOrBlockId === 'string' + ? await this._editor.getBlockById(blockOrBlockId) + : blockOrBlockId; + const blockView = this._editor.getView(block.type); + + return blockView.activatable; + } + + async getClipInfoOfBlockById(blockId: string) { + const block = await this._editor.getBlockById(blockId); + const blockInfo: ClipBlockInfo = { + type: block.type, + properties: block?.getProperties(), + children: [] as ClipBlockInfo[], + }; + const children = (await block?.children()) ?? []; + + await Promise.all( + children.map(async childBlock => { + const childInfo = await this.getClipInfoOfBlockById( + childBlock.id + ); + blockInfo.children.push(childInfo); + }) + ); + + return blockInfo; + } + + async getClipDataOfBlocksById(blockIds: string[]) { + const clipInfos = await Promise.all( + blockIds.map(blockId => this.getClipInfoOfBlockById(blockId)) + ); + + return new Clip( + OFFICE_CLIPBOARD_MIMETYPE.DOCS_DOCUMENT_SLICE_CLIP_WRAPPED, + JSON.stringify({ + data: clipInfos, + }) + ); + } + + async getClipInfoOfBlockBySelectInfo(selectBlockInfo: SelectBlock) { + const block = await this._editor.getBlockById(selectBlockInfo.blockId); + const blockInfo: ClipBlockInfo = { + type: block?.type, + properties: + await this._editor.blockHelper.getBlockPropertiesBySelectInfo( + selectBlockInfo + ), + // Editable has no children + children: [], + }; + return blockInfo; + } + + async getClipDataOfBlocksBySelectInfo(selectInfo: SelectInfo) { + const clipInfos = await Promise.all( + selectInfo.blocks.map(selectBlockInfo => + this.getClipInfoOfBlockBySelectInfo(selectBlockInfo) + ) + ); + return new Clip( + OFFICE_CLIPBOARD_MIMETYPE.DOCS_DOCUMENT_SLICE_CLIP_WRAPPED, + JSON.stringify({ + data: clipInfos, + }) + ); + } + + async convertEditableBlockToHtml(block: AsyncBlock) { + const generate = (textList: any[]) => { + let content = ''; + textList.forEach(text_obj => { + let text = text_obj.text || ''; + if (text_obj.bold) { + text = `${text}`; + } + if (text_obj.italic) { + text = `${text}`; + } + if (text_obj.underline) { + text = `${text}`; + } + if (text_obj.inlinecode) { + text = `${text}`; + } + if (text_obj.strikethrough) { + text = `${text}`; + } + if (text_obj.type === 'link') { + text = `${generate( + text_obj.children + )}`; + } + content += text; + }); + return content; + }; + const text_list: any[] = block.getProperty('text').value; + return generate(text_list); + } +} diff --git a/libs/components/editor-core/src/editor/clipboard/copy.ts b/libs/components/editor-core/src/editor/clipboard/copy.ts index 94b3117264..8509af23fc 100644 --- a/libs/components/editor-core/src/editor/clipboard/copy.ts +++ b/libs/components/editor-core/src/editor/clipboard/copy.ts @@ -4,15 +4,15 @@ import { OFFICE_CLIPBOARD_MIMETYPE } from './types'; import { Clip } from './clip'; import ClipboardParse from './clipboard-parse'; import { getClipDataOfBlocksById } from './utils'; -import { copyToClipboard } from '@toeverything/utils'; +import { ClipboardUtils } from './clipboardUtils'; class Copy { private _editor: Editor; private _clipboardParse: ClipboardParse; - + private _utils: ClipboardUtils; constructor(editor: Editor) { this._editor = editor; this._clipboardParse = new ClipboardParse(editor); - + this._utils = new ClipboardUtils(editor); this.handleCopy = this.handleCopy.bind(this); } public async handleCopy(e: ClipboardEvent) { @@ -22,7 +22,6 @@ class Copy { if (!clips.length) { return; } - // TODO: is not compatible with safari const success = this._copyToClipboardFromPc(clips); if (!success) { // This way, not compatible with firefox @@ -49,7 +48,11 @@ class Copy { const affineClip = await this._getAffineClip(); clips.push(affineClip); - // get common html clip + const textClip = await this._getTextClip(); + clips.push(textClip); + + // const htmlClip = await this._getHtmlClip(); + // clips.push(htmlClip); const htmlClip = await this._clipboardParse.generateHtml(); htmlClip && clips.push(new Clip(OFFICE_CLIPBOARD_MIMETYPE.HTML, htmlClip)); @@ -57,16 +60,128 @@ class Copy { return clips; } + // private async _getHtmlClip(): Promise { + // const selectInfo: SelectInfo = + // await this._editor.selectionManager.getSelectInfo(); + // + // if (selectInfo.type === 'Range') { + // const html = ( + // await Promise.all( + // selectInfo.blocks.map(async selectBlockInfo => { + // const block = await this._editor.getBlockById( + // selectBlockInfo.blockId + // ); + // const blockView = this._editor.getView(block.type); + // const block2html = await blockView.block2html({ + // editor: this._editor, + // block, + // selectInfo: selectBlockInfo, + // }); + // + // if ( + // await this._editor.blockHelper.isBlockEditable( + // block + // ) + // ) { + // const selectedProperties = + // await this._editor.blockHelper.getEditableBlockPropertiesBySelectInfo( + // block, + // selectBlockInfo + // ); + // + // return ( + // block2html || + // this._editor.blockHelper.convertTextValue2Html( + // block.id, + // selectedProperties.text.value + // ) + // ); + // } + // + // return block2html; + // }) + // ) + // ).join(''); + // console.log('html', html); + // } + // + // return new Clip(OFFICE_CLIPBOARD_MIMETYPE.HTML, 'blockText'); + // } + private async _getAffineClip(): Promise { const selectInfo: SelectInfo = await this._editor.selectionManager.getSelectInfo(); - return getClipDataOfBlocksById( - this._editor, + if (selectInfo.type === 'Range') { + return this._utils.getClipDataOfBlocksBySelectInfo(selectInfo); + } + + // The only remaining case is that selectInfo.type === 'Block' + return this._utils.getClipDataOfBlocksById( selectInfo.blocks.map(block => block.blockId) ); } + private async _getTextClip(): Promise { + const selectInfo: SelectInfo = + await this._editor.selectionManager.getSelectInfo(); + + if (selectInfo.type === 'Range') { + const text = ( + await Promise.all( + selectInfo.blocks.map(async selectBlockInfo => { + const block = await this._editor.getBlockById( + selectBlockInfo.blockId + ); + const blockView = this._editor.getView(block.type); + const block2Text = await blockView.block2Text( + block, + selectBlockInfo + ); + + return ( + block2Text || + this._editor.blockHelper.getBlockTextBetweenSelection( + selectBlockInfo.blockId, + false + ) + ); + }) + ) + ).join('\n'); + return new Clip(OFFICE_CLIPBOARD_MIMETYPE.TEXT, text); + } + + // The only remaining case is that selectInfo.type === 'Block' + const selectedBlocks = ( + await Promise.all( + selectInfo.blocks.map(selectBlockInfo => + this._editor.blockHelper.getFlatBlocksUnderParent( + selectBlockInfo.blockId, + true + ) + ) + ) + ).flat(); + + const blockText = ( + await Promise.all( + selectedBlocks.map(async block => { + const blockView = this._editor.getView(block.type); + const block2Text = await blockView.block2Text(block); + return ( + block2Text || + this._editor.blockHelper.getBlockText(block.id) + ); + }) + ) + ).join('\n'); + + return new Clip(OFFICE_CLIPBOARD_MIMETYPE.TEXT, blockText); + } + + // TODO: Optimization + // TODO: is not compatible with safari private _copyToClipboardFromPc(clips: any[]) { let success = false; const tempElem = document.createElement('textarea'); diff --git a/libs/components/editor-core/src/editor/clipboard/index.ts b/libs/components/editor-core/src/editor/clipboard/index.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/components/editor-core/src/editor/clipboard/paste.ts b/libs/components/editor-core/src/editor/clipboard/paste.ts index 37168c2614..0d631991c2 100644 --- a/libs/components/editor-core/src/editor/clipboard/paste.ts +++ b/libs/components/editor-core/src/editor/clipboard/paste.ts @@ -42,13 +42,13 @@ export class Paste { OFFICE_CLIPBOARD_MIMETYPE.HTML, OFFICE_CLIPBOARD_MIMETYPE.TEXT, ]; - public handlePaste(e: Event) { + public handlePaste(e: ClipboardEvent) { if (!shouldHandlerContinue(e, this._editor)) { return; } e.stopPropagation(); - const clipboardData = (e as ClipboardEvent).clipboardData; + const clipboardData = e.clipboardData; const isPureFile = Paste._isPureFileInClipboard(clipboardData); if (isPureFile) { diff --git a/libs/components/editor-core/src/editor/clipboard/utils.ts b/libs/components/editor-core/src/editor/clipboard/utils.ts index c807a869a0..548e187814 100644 --- a/libs/components/editor-core/src/editor/clipboard/utils.ts +++ b/libs/components/editor-core/src/editor/clipboard/utils.ts @@ -2,7 +2,10 @@ import { Editor } from '../editor'; import { ClipBlockInfo, OFFICE_CLIPBOARD_MIMETYPE } from './types'; import { Clip } from './clip'; -export const shouldHandlerContinue = (event: Event, editor: Editor) => { +export const shouldHandlerContinue = ( + event: ClipboardEvent, + editor: Editor +) => { const filterNodes = ['INPUT', 'SELECT', 'TEXTAREA']; if (event.defaultPrevented) { @@ -20,10 +23,9 @@ export const getClipInfoOfBlockById = async ( blockId: string ) => { const block = await editor.getBlockById(blockId); - const blockView = editor.getView(block.type); const blockInfo: ClipBlockInfo = { type: block.type, - properties: blockView.getSelProperties(block, {}), + properties: block?.getProperties(), children: [] as ClipBlockInfo[], }; const children = (await block?.children()) ?? []; diff --git a/libs/components/editor-core/src/editor/editor.ts b/libs/components/editor-core/src/editor/editor.ts index e85db2ba65..931fd25da0 100644 --- a/libs/components/editor-core/src/editor/editor.ts +++ b/libs/components/editor-core/src/editor/editor.ts @@ -15,8 +15,7 @@ import assert from 'assert'; import type { WorkspaceAndBlockId } from './block'; import { AsyncBlock } from './block'; import { BlockHelper } from './block/block-helper'; -import { BrowserClipboard } from './clipboard/browser-clipboard'; -import { ClipboardPopulator } from './clipboard/clipboard-populator'; +import { Clipboard } from './clipboard/clipboard'; import { EditorCommands } from './commands'; import { EditorConfig } from './config'; import { DragDropManager } from './drag-drop'; @@ -65,8 +64,9 @@ export class Editor implements Virgo { readonly = false; private _rootBlockId: string; private storage_manager?: StorageManager; - private clipboard?: BrowserClipboard; - private clipboard_populator?: ClipboardPopulator; + private _clipboard: Clipboard; + // private clipboardActionDispacher?: ClipboardEventDispatcher; + // private clipboard_populator?: ClipboardPopulator; public reactRenderRoot: { render: PatchNode; has: (key: string) => boolean; @@ -138,7 +138,7 @@ export class Editor implements Virgo { public set container(v: HTMLDivElement) { this.ui_container = v; - this._initClipboard(); + this._clipboard = new Clipboard(this, this.ui_container); } public get container() { @@ -191,26 +191,26 @@ export class Editor implements Virgo { }; } - private _disposeClipboard() { - this.clipboard?.dispose(); - this.clipboard_populator?.disposeInternal(); - } + // private _disposeClipboard() { + // this.clipboard?.dispose(); + // this.clipboard_populator?.disposeInternal(); + // } - private _initClipboard() { - this._disposeClipboard(); - if (this.ui_container && !this._isDisposed) { - this.clipboard = new BrowserClipboard( - this.ui_container, - this.hooks, - this - ); - this.clipboard_populator = new ClipboardPopulator( - this, - this.hooks, - this.selectionManager - ); - } - } + // private _initClipboard() { + // this._disposeClipboard(); + // if (this.ui_container && !this._isDisposed) { + // this.clipboardActionDispacher = new ClipboardEventDispatcher({ + // clipboardTarget: this.ui_container, + // hooks: this.hooks, + // editor: this, + // }); + // this.clipboard_populator = new ClipboardPopulator( + // this, + // this.hooks, + // this.selectionManager + // ); + // } + // } /** Root Block Id */ getRootBlockId() { @@ -498,12 +498,13 @@ export class Editor implements Virgo { } public async page2html(): Promise { - const parse = this.clipboard?.getClipboardParse(); - if (!parse) { - return ''; - } - const html_str = await parse.page2html(); - return html_str; + return ''; + // const parse = this.clipboard?.getClipboardParse(); + // if (!parse) { + // return ''; + // } + // const html_str = await parse.page2html(); + // return html_str; } dispose() { @@ -519,6 +520,6 @@ export class Editor implements Virgo { this.plugin_manager.dispose(); this.selectionManager.dispose(); this.dragDropManager.dispose(); - this._disposeClipboard(); + this._clipboard?.dispose(); } } diff --git a/libs/components/editor-core/src/editor/index.ts b/libs/components/editor-core/src/editor/index.ts index 3131e6f0a1..08c479ba8f 100644 --- a/libs/components/editor-core/src/editor/index.ts +++ b/libs/components/editor-core/src/editor/index.ts @@ -8,6 +8,6 @@ export { Editor as BlockEditor } from './editor'; export * from './selection'; export { BlockDropPlacement, HookType, GroupDirection } from './types'; export type { Plugin, PluginCreator, PluginHooks, Virgo } from './types'; -export { BaseView, getTextHtml, getTextProperties } from './views/base-view'; +export { BaseView, getTextHtml } from './views/base-view'; export type { ChildrenView, CreateView } from './views/base-view'; export { getClipDataOfBlocksById } from './clipboard/utils'; diff --git a/libs/components/editor-core/src/editor/plugin/hooks.ts b/libs/components/editor-core/src/editor/plugin/hooks.ts index 42b9c262a0..747f0b42e2 100644 --- a/libs/components/editor-core/src/editor/plugin/hooks.ts +++ b/libs/components/editor-core/src/editor/plugin/hooks.ts @@ -116,12 +116,15 @@ export class Hooks implements HooksRunner, PluginHooks { this._runHook(HookType.ON_SEARCH); } - public beforeCopy(e: ClipboardEvent): void { - this._runHook(HookType.BEFORE_COPY, e); + public onCopy(e: ClipboardEvent): void { + this._runHook(HookType.ON_COPY, e); } - public beforeCut(e: ClipboardEvent): void { - this._runHook(HookType.BEFORE_CUT, e); + public onCut(e: ClipboardEvent): void { + this._runHook(HookType.ON_CUT, e); + } + public onPaste(e: ClipboardEvent): void { + this._runHook(HookType.ON_PASTE, e); } public onRootNodeScroll(e: React.UIEvent): void { diff --git a/libs/components/editor-core/src/editor/types.ts b/libs/components/editor-core/src/editor/types.ts index 1b97cf0f86..f2621164b6 100644 --- a/libs/components/editor-core/src/editor/types.ts +++ b/libs/components/editor-core/src/editor/types.ts @@ -174,8 +174,9 @@ export enum HookType { ON_ROOTNODE_DRAG_END = 'onRootNodeDragEnd', ON_ROOTNODE_DRAG_OVER_CAPTURE = 'onRootNodeDragOverCapture', ON_ROOTNODE_DROP = 'onRootNodeDrop', - BEFORE_COPY = 'beforeCopy', - BEFORE_CUT = 'beforeCut', + ON_COPY = 'onCopy', + ON_CUT = 'onCut', + ON_PASTE = 'onPaste', ON_ROOTNODE_SCROLL = 'onRootNodeScroll', } @@ -207,8 +208,9 @@ export interface HooksRunner { onRootNodeDragEnd: (e: React.DragEvent) => void; onRootNodeDragLeave: (e: React.DragEvent) => void; onRootNodeDrop: (e: React.DragEvent) => void; - beforeCopy: (e: ClipboardEvent) => void; - beforeCut: (e: ClipboardEvent) => void; + onCopy: (e: ClipboardEvent) => void; + onCut: (e: ClipboardEvent) => void; + onPaste: (e: ClipboardEvent) => void; onRootNodeScroll: (e: React.UIEvent) => void; } diff --git a/libs/components/editor-core/src/editor/views/base-view.ts b/libs/components/editor-core/src/editor/views/base-view.ts index b69cdd848a..f9db79fbc5 100644 --- a/libs/components/editor-core/src/editor/views/base-view.ts +++ b/libs/components/editor-core/src/editor/views/base-view.ts @@ -131,10 +131,6 @@ export abstract class BaseView { return result; } - getSelProperties(block: AsyncBlock, selectInfo: any): DefaultColumnsValue { - return cloneDeep(block.getProperties()); - } - html2block(el: Element, parseEl: (el: Element) => any[]): any[] | null { return null; } @@ -146,31 +142,24 @@ export abstract class BaseView { ): Promise { return ''; } -} + async block2Text( + block: AsyncBlock, + // The selectInfo parameter is not passed when the block is selected in ful, the selectInfo.type is Range + selectInfo?: SelectBlock + ): Promise { + return ''; + } -export const getTextProperties = ( - properties: DefaultColumnsValue, - selectInfo: any -) => { - let text_value = properties.text.value; - if (text_value.length === 0) { - return properties; - } - if (selectInfo.endInfo) { - text_value = text_value.slice(0, selectInfo.endInfo.arrayIndex + 1); - text_value[text_value.length - 1].text = text_value[ - text_value.length - 1 - ].text.substring(0, selectInfo.endInfo.offset); - } - if (selectInfo.startInfo) { - text_value = text_value.slice(selectInfo.startInfo.arrayIndex); - text_value[0].text = text_value[0].text.substring( - selectInfo.startInfo.offset - ); - } - properties.text.value = text_value; - return properties; -}; + // TODO: Try using new methods + // async block2html2(props: { + // editor: Editor; + // block: AsyncBlock; + // // The selectInfo parameter is not passed when the block is selected in ful, the selectInfo.type is Range + // selectInfo?: SelectBlock; + // }) { + // return ''; + // } +} export const getTextHtml = (block: AsyncBlock) => { const generate = (textList: any[]) => {