diff --git a/libs/components/common/src/lib/text/slate-utils.ts b/libs/components/common/src/lib/text/slate-utils.ts index 3f5fb57697..1783d79cab 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-blocks/src/components/text-manage/TextManage.tsx b/libs/components/editor-blocks/src/components/text-manage/TextManage.tsx index 7928375a32..99179220a9 100644 --- a/libs/components/editor-blocks/src/components/text-manage/TextManage.tsx +++ b/libs/components/editor-blocks/src/components/text-manage/TextManage.tsx @@ -45,13 +45,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; } }); }; @@ -115,11 +115,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 { @@ -128,18 +125,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 { @@ -154,24 +151,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; @@ -189,7 +186,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, { @@ -197,19 +194,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) { @@ -235,17 +232,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 ) => { @@ -266,39 +263,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) { @@ -339,7 +331,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 @@ -395,7 +387,7 @@ export const TextManage = forwardRef( return false; } }; - const on_select_all = () => { + const onSelectAll = () => { const isSelectAll = textRef.current.isEmpty() || textRef.current.isSelectAll(); if (isSelectAll) { @@ -405,22 +397,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 { @@ -428,7 +418,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) { @@ -477,16 +467,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} /> ); 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 1c1ab37dbe..7bd0284ffc 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/browser-clipboard.ts b/libs/components/editor-core/src/editor/clipboard/browser-clipboard.ts index eefca42b0e..64eecbc5ca 100644 --- a/libs/components/editor-core/src/editor/clipboard/browser-clipboard.ts +++ b/libs/components/editor-core/src/editor/clipboard/browser-clipboard.ts @@ -14,403 +14,126 @@ 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; enum ClipboardAction { COPY = 'copy', CUT = 'cut', PASTE = 'paste', } -class BrowserClipboard { - private event_target: Element; - private hooks: HooksRunner; - private editor: Editor; - private clipboard_parse: ClipboardParse; - private markdown_parse: MarkdownParser; - private static optimal_mime_type: string[] = [ - OFFICE_CLIPBOARD_MIMETYPE.DOCS_DOCUMENT_SLICE_CLIP_WRAPPED, - OFFICE_CLIPBOARD_MIMETYPE.HTML, - OFFICE_CLIPBOARD_MIMETYPE.TEXT, - ]; +//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.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._paste = new Paste( + editor, + this._clipboardParse, + this._markdownParse + ); + 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); - 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( - ClipboardAction.COPY, - this.handle_copy - ); - this.event_target.addEventListener( - ClipboardAction.CUT, - this.handle_cut - ); - this.event_target.addEventListener( + document.addEventListener(ClipboardAction.COPY, this._handleCopy); + document.addEventListener(ClipboardAction.CUT, this._handleCut); + document.addEventListener( ClipboardAction.PASTE, - this.handle_paste + this._paste.handlePaste ); - } - - private handle_copy(e: Event) { - //@ts-ignore - if (e.defaultPrevented || e.target.nodeName === 'INPUT') { - return; - } - - this.dispatch_clipboard_event( + this._eventTarget.addEventListener( ClipboardAction.COPY, - e as ClipboardEvent + this._handleCopy + ); + this._eventTarget.addEventListener( + ClipboardAction.CUT, + this._handleCut + ); + this._eventTarget.addEventListener( + ClipboardAction.PASTE, + this._paste.handlePaste ); } - private handle_cut(e: Event) { - //@ts-ignore - if (e.defaultPrevented || e.target.nodeName === 'INPUT') { - 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') { + private _handleCopy(e: Event) { + if (!shouldHandlerContinue(e, this._editor)) { return; } - const clipboardData = (e as ClipboardEvent).clipboardData; - - const isPureFile = this.is_pure_file_in_clipboard(clipboardData); - - if (!isPureFile) { - this.paste_content(clipboardData); - } else { - this.paste_file(clipboardData); - } - // this.editor.selectionManager - // .getSelectInfo() - // .then(selectionInfo => console.log(selectionInfo)); - - e.stopPropagation(); + this._dispatchClipboardEvent(ClipboardAction.COPY, e as ClipboardEvent); } - private paste_content(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.excape_html(clipData); - } - - switch (originClip['type']) { - /** Protocol paste */ - case OFFICE_CLIPBOARD_MIMETYPE.DOCS_DOCUMENT_SLICE_CLIP_WRAPPED: - this.fire_paste_edit_action(clipData); - break; - case OFFICE_CLIPBOARD_MIMETYPE.HTML: - this.paste_html(clipData, originTextClipData); - break; - case OFFICE_CLIPBOARD_MIMETYPE.TEXT: - this.paste_text(clipData, originTextClipData); - break; - - default: - break; - } - } - - private paste_html(clipData: any, originTextClipData: any) { - if (support_markdown_paste) { - const has_markdown = - this.markdown_parse.checkIfTextContainsMd(originTextClipData); - if (has_markdown) { - try { - const convertedDataObj = - this.markdown_parse.md2Html(originTextClipData); - if (convertedDataObj.isConverted) { - clipData = convertedDataObj.text; - } - } catch (e) { - console.error(e); - clipData = originTextClipData; - } - } - } - - const blocks = this.clipboard_parse.html2blocks(clipData); - this.insert_blocks(blocks); - } - - private paste_text(clipData: any, originTextClipData: any) { - const blocks = this.clipboard_parse.text2blocks(clipData); - this.insert_blocks(blocks); - } - - private async paste_file(clipboardData: any) { - const file = this.get_image_file(clipboardData); - if (file) { - const result = await services.api.file.create({ - workspace: this.editor.workspace, - file: file, - }); - const block_info: ClipBlockInfo = { - type: 'image', - properties: { - image: { - value: result.id, - name: file.name, - size: file.size, - type: file.type, - }, - }, - children: [] as ClipBlockInfo[], - }; - this.insert_blocks([block_info]); - } - } - - private get_image_file(clipboardData: any) { - const files = clipboardData.files; - if (files && files[0] && files[0].type.indexOf('image') > -1) { - return files[0]; - } - return; - } - - private excape_html(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.optimal_mime_type; - - 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 is_pure_file_in_clipboard(clipboardData: DataTransfer) { - const types = clipboardData.types; - - const res = - (types.length === 1 && types[0] === 'Files') || - (types.length === 2 && - (types.includes('text/plain') || types.includes('text/html')) && - types.includes('Files')); - - return res; - } - - 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 can_edit_text(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) { + private _handleCut(e: Event) { + if (!shouldHandlerContinue(e, this._editor)) { return; } - const cur_select_info = - await this.editor.selectionManager.getSelectInfo(); - if (cur_select_info.blocks.length === 0) { - return; - } - - let begin_index = 0; - const cur_node_id = - 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); - if ( - cur_select_info.type === 'Range' && - cur_block.type === 'text' && - block_view.isEmpty(cur_block) - ) { - cur_block.setType(blocks[0].type); - cur_block.setProperties(blocks[0].properties); - this.paste_children(cur_block, blocks[0].children); - begin_index = 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) - ) { - 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( - 0, - start_info.arrayIndex - ); - const last_cur_text_value = cur_text_value.slice( - end_info.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); - - 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); - } - 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 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); - setTimeout(async () => { - const cur_block = await this.editor.getBlockById( - cur_node_id - ); - cur_block.setProperties({ - text: { value: pre_cur_text_value }, - }); - this.paste_children(cur_block, blocks[0].children); - }, 0); - begin_index = 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); - } else { - cur_block.after(next_block); - } - - this.paste_children(next_block, blocks[i].children); - cur_block = next_block; - } + this._dispatchClipboardEvent(ClipboardAction.CUT, e as ClipboardEvent); } - private async paste_children(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); - } - } - - 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( - ClipboardAction.COPY, - this.handle_copy - ); - this.event_target.removeEventListener( - ClipboardAction.CUT, - this.handle_cut - ); - this.event_target.removeEventListener( + document.removeEventListener(ClipboardAction.COPY, this._handleCopy); + document.removeEventListener(ClipboardAction.CUT, this._handleCut); + document.removeEventListener( ClipboardAction.PASTE, - this.handle_paste + this._paste.handlePaste ); - this.clipboard_parse.dispose(); - this.clipboard_parse = null; - this.event_target = null; - this.hooks = null; - this.editor = null; + 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; } } 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 a4a59bd2ba..43b72ed6e7 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; } 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 4d28ce3cf8..2f05866ab9 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; } diff --git a/libs/components/editor-core/src/editor/clipboard/paste.ts b/libs/components/editor-core/src/editor/clipboard/paste.ts new file mode 100644 index 0000000000..0c48eee56b --- /dev/null +++ b/libs/components/editor-core/src/editor/clipboard/paste.ts @@ -0,0 +1,455 @@ +/* eslint-disable max-lines */ +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(); + + // 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( + 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: '' }]; + const { startInfo, endInfo } = currentSelectInfo.blocks[0]; + + // 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[], + 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 { + this._editor.blockHelper.insertNodes( + selectedBlock.id, + pureText, + { select: true } + ); + } + } + // 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[], + 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 { + this._editor.blockHelper.insertNodes( + selectedBlock.id, + pureText, + { select: true } + ); + } + } + } else { + const pasteBlocks = await this._createBlocks(blocks); + pasteBlocks.forEach(block => { + selectedBlock.after(block); + }); + this._setEndSelectToBlock( + pasteBlocks[pasteBlocks.length - 1].id + ); + } + } + + if (currentSelectInfo.type === 'Block') { + const selectedBlock = await this._editor.getBlockById( + currentSelectInfo.blocks[currentSelectInfo.blocks.length - 1] + .blockId + ); + const pasteBlocks = await this._createBlocks(blocks); + + let groupBlock: AsyncBlock; + if ( + selectedBlock?.type === 'group' || + selectedBlock?.type === 'page' + ) { + groupBlock = await this._editor.createBlock('group'); + pasteBlocks.forEach(block => { + groupBlock.append(block); + }); + await selectedBlock.after(groupBlock); + } else { + pasteBlocks.forEach(block => { + selectedBlock.after(block); + }); + } + this._setEndSelectToBlock(pasteBlocks[pasteBlocks.length - 1].id); + } + } + + private _setEndSelectToBlock(blockId: string) { + setTimeout(() => { + this._editor.selectionManager.activeNodeByNodeId(blockId, 'end'); + }, 100); + } + + private async _createBlocks(blocks: ClipBlockInfo[], parentId?: string) { + return Promise.all( + blocks.map(async clipBlockInfo => { + const block = await this._editor.createBlock( + clipBlockInfo.type + ); + block?.setProperties(clipBlockInfo.properties); + await this._createBlocks(clipBlockInfo.children, block?.id); + return block; + }) + ); + } + + private async _pasteHtml(clipData: any, originTextClipData: any) { + if (SUPPORT_MARKDOWN_PASTE) { + const hasMarkdown = + this._markdownParse.checkIfTextContainsMd(originTextClipData); + if (hasMarkdown) { + try { + const convertedDataObj = + this._markdownParse.md2Html(originTextClipData); + if (convertedDataObj.isConverted) { + clipData = convertedDataObj.text; + } + } catch (e) { + console.error(e); + clipData = originTextClipData; + } + } + } + + const blocks = this._clipboardParse.html2blocks(clipData); + + await this._insertBlocks(blocks); + } + + private async _pasteText(clipData: any, originTextClipData: any) { + const blocks = this._clipboardParse.text2blocks(clipData); + await this._insertBlocks(blocks); + } + + private static _getImageFile(clipboardData: any) { + const files = clipboardData.files; + if (files && files[0] && files[0].type.indexOf('image') > -1) { + return files[0]; + } + return; + } + + private static _excapeHtml(data: any, onlySpace?: any) { + if (!onlySpace) { + // TODO: + // data = string.htmlEscape(data); + // data = data.replace(/\n/g, '
'); + } + + // data = data.replace(/ /g, ' '); + // data = data.replace(/\t/g, '    '); + return data; + } +} diff --git a/libs/components/editor-core/src/editor/clipboard/utils.ts b/libs/components/editor-core/src/editor/clipboard/utils.ts new file mode 100644 index 0000000000..cb5d10241e --- /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'; +}; diff --git a/libs/components/editor-core/src/editor/selection/selection.ts b/libs/components/editor-core/src/editor/selection/selection.ts index 30e7f4efdd..5b5520bcf3 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< @@ -1047,4 +1048,25 @@ export class SelectionManager implements VirgoSelection { this._windowSelectionChangeHandler ); } + /** + * + * move active selection to the new position + * @param {number} index + * @param {string} blockId + * @memberof SelectionManager + */ + public async moveCursor( + nowRange: any, + index: number, + blockId: string + ): Promise { + let preRang = document.createRange(); + preRang.setStart(nowRange.startContainer, index); + preRang.setEnd(nowRange.endContainer, index); + let prePosition = preRang.getClientRects().item(0); + this.activeNodeByNodeId( + blockId, + new Point(prePosition.left, prePosition.bottom) + ); + } }