From 8989831474f637fe52133abc85d1ed2dc41f6354 Mon Sep 17 00:00:00 2001 From: Hufe921 Date: Wed, 8 Nov 2023 21:39:36 +0800 Subject: [PATCH] feat: support for paste richtext data in contextmenu --- docs/en/guide/command-execute.md | 2 +- docs/en/guide/override.md | 2 +- docs/guide/command-execute.md | 2 +- docs/guide/override.md | 2 +- src/editor/core/command/CommandAdapt.ts | 9 +- src/editor/core/cursor/CursorAgent.ts | 101 +------------- src/editor/core/event/handlers/paste.ts | 176 ++++++++++++++++++++++++ src/editor/core/override/Override.ts | 2 +- src/editor/interface/Event.ts | 3 + 9 files changed, 190 insertions(+), 109 deletions(-) create mode 100644 src/editor/core/event/handlers/paste.ts create mode 100644 src/editor/interface/Event.ts diff --git a/docs/en/guide/command-execute.md b/docs/en/guide/command-execute.md index 77960df..c04f5f0 100644 --- a/docs/en/guide/command-execute.md +++ b/docs/en/guide/command-execute.md @@ -46,7 +46,7 @@ Feature: Paste Usage: ```javascript -instance.command.executePaste() +instance.command.executePaste(payload?: IPasteOption) ``` ## executeSelectAll diff --git a/docs/en/guide/override.md b/docs/en/guide/override.md index 48a0101..abd982c 100644 --- a/docs/en/guide/override.md +++ b/docs/en/guide/override.md @@ -16,7 +16,7 @@ Feature: Override internal paste function Usage: ```javascript -instance.override.paste = (evt: ClipboardEvent) => void +instance.override.paste = (evt?: ClipboardEvent) => void ``` ## copy diff --git a/docs/guide/command-execute.md b/docs/guide/command-execute.md index d39b46e..1205da0 100644 --- a/docs/guide/command-execute.md +++ b/docs/guide/command-execute.md @@ -46,7 +46,7 @@ instance.command.executeCopy() 用法: ```javascript -instance.command.executePaste() +instance.command.executePaste(payload?: IPasteOption) ``` ## executeSelectAll diff --git a/docs/guide/override.md b/docs/guide/override.md index 4786475..d85621f 100644 --- a/docs/guide/override.md +++ b/docs/guide/override.md @@ -16,7 +16,7 @@ instance.override.overrideFunction = ()=>{} 用法: ```javascript -instance.override.paste = (evt: ClipboardEvent) => void +instance.override.paste = (evt?: ClipboardEvent) => void ``` ## copy diff --git a/src/editor/core/command/CommandAdapt.ts b/src/editor/core/command/CommandAdapt.ts index 02aa15b..25e8c61 100644 --- a/src/editor/core/command/CommandAdapt.ts +++ b/src/editor/core/command/CommandAdapt.ts @@ -40,6 +40,7 @@ import { IEditorText } from '../../interface/Editor' import { IElement, IElementStyle } from '../../interface/Element' +import { IPasteOption } from '../../interface/Event' import { IMargin } from '../../interface/Margin' import { RangeContext, RangeRect } from '../../interface/Range' import { IColgroup } from '../../interface/table/Colgroup' @@ -63,6 +64,7 @@ import { Draw } from '../draw/Draw' import { INavigateInfo, Search } from '../draw/interactive/Search' import { TableTool } from '../draw/particle/table/TableTool' import { CanvasEvent } from '../event/CanvasEvent' +import { pasteByApi } from '../event/handlers/paste' import { HistoryManager } from '../history/HistoryManager' import { I18n } from '../i18n/I18n' import { Position } from '../position/Position' @@ -110,13 +112,10 @@ export class CommandAdapt { this.canvasEvent.copy() } - public async paste() { + public paste(payload?: IPasteOption) { const isReadonly = this.draw.isReadonly() if (isReadonly) return - const text = await navigator.clipboard.readText() - if (text) { - this.canvasEvent.input(text) - } + pasteByApi(this.canvasEvent, payload) } public selectAll() { diff --git a/src/editor/core/cursor/CursorAgent.ts b/src/editor/core/cursor/CursorAgent.ts index 898e561..b351773 100644 --- a/src/editor/core/cursor/CursorAgent.ts +++ b/src/editor/core/cursor/CursorAgent.ts @@ -1,12 +1,8 @@ -import { ZERO } from '../../dataset/constant/Common' import { EDITOR_PREFIX } from '../../dataset/constant/Editor' -import { VIRTUAL_ELEMENT_TYPE } from '../../dataset/constant/Element' -import { ElementType } from '../../dataset/enum/Element' -import { IElement } from '../../interface/Element' import { debounce } from '../../utils' -import { formatElementContext, getElementListByHTML } from '../../utils/element' import { Draw } from '../draw/Draw' import { CanvasEvent } from '../event/CanvasEvent' +import { pasteByEvent } from '../event/handlers/paste' export class CursorAgent { private draw: Draw @@ -57,100 +53,7 @@ export class CursorAgent { if (isReadonly) return const clipboardData = evt.clipboardData if (!clipboardData) return - // 自定义粘贴事件 - const { paste } = this.draw.getOverride() - if (paste) { - paste(evt) - return - } - const rangeManager = this.draw.getRange() - const { startIndex } = rangeManager.getRange() - const elementList = this.draw.getElementList() - // 从粘贴板提取数据 - let isHTML = false - for (let i = 0; i < clipboardData.items.length; i++) { - const item = clipboardData.items[i] - if (item.type === 'text/html') { - isHTML = true - break - } - } - for (let i = 0; i < clipboardData.items.length; i++) { - const item = clipboardData.items[i] - if (item.kind === 'string') { - if (item.type === 'text/plain' && !isHTML) { - item.getAsString(plainText => { - this.canvasEvent.input(plainText) - }) - } - if (item.type === 'text/html' && isHTML) { - item.getAsString(htmlText => { - const pasteElementList = getElementListByHTML(htmlText, { - innerWidth: this.draw.getOriginalInnerWidth() - }) - // 全选粘贴无需格式化上下文 - if (~startIndex && !rangeManager.getIsSelectAll()) { - // 如果是复制到虚拟元素里,则粘贴列表的虚拟元素需扁平化处理,避免产生新的虚拟元素 - const anchorElement = elementList[startIndex] - if (anchorElement?.titleId || anchorElement?.listId) { - let start = 0 - while (start < pasteElementList.length) { - const pasteElement = pasteElementList[start] - if (anchorElement.titleId && /^\n/.test(pasteElement.value)) { - break - } - if (VIRTUAL_ELEMENT_TYPE.includes(pasteElement.type!)) { - pasteElementList.splice(start, 1) - if (pasteElement.valueList) { - for (let v = 0; v < pasteElement.valueList.length; v++) { - const element = pasteElement.valueList[v] - if (element.value === ZERO || element.value === '\n') { - continue - } - pasteElementList.splice(start, 0, element) - start++ - } - } - start-- - } - start++ - } - } - formatElementContext(elementList, pasteElementList, startIndex, { - isBreakWhenWrap: true - }) - } - this.draw.insertElementList(pasteElementList) - }) - } - } else if (item.kind === 'file') { - if (item.type.includes('image')) { - const file = item.getAsFile() - if (file) { - const fileReader = new FileReader() - fileReader.readAsDataURL(file) - fileReader.onload = () => { - // 计算宽高 - const image = new Image() - const value = fileReader.result as string - image.src = value - image.onload = () => { - const imageElement: IElement = { - value, - type: ElementType.IMAGE, - width: image.width, - height: image.height - } - if (~startIndex) { - formatElementContext(elementList, [imageElement], startIndex) - } - this.draw.insertElementList([imageElement]) - } - } - } - } - } - } + pasteByEvent(this.canvasEvent, evt) evt.preventDefault() } diff --git a/src/editor/core/event/handlers/paste.ts b/src/editor/core/event/handlers/paste.ts new file mode 100644 index 0000000..927f03c --- /dev/null +++ b/src/editor/core/event/handlers/paste.ts @@ -0,0 +1,176 @@ +import { ZERO } from '../../../dataset/constant/Common' +import { VIRTUAL_ELEMENT_TYPE } from '../../../dataset/constant/Element' +import { ElementType } from '../../../dataset/enum/Element' +import { IElement } from '../../../interface/Element' +import { IPasteOption } from '../../../interface/Event' +import { + formatElementContext, + getElementListByHTML +} from '../../../utils/element' +import { CanvasEvent } from '../CanvasEvent' + +export function pastHTML(host: CanvasEvent, htmlText: string) { + const draw = host.getDraw() + const isReadonly = draw.isReadonly() + if (isReadonly) return + const rangeManager = draw.getRange() + const { startIndex } = rangeManager.getRange() + const elementList = draw.getElementList() + const pasteElementList = getElementListByHTML(htmlText, { + innerWidth: draw.getOriginalInnerWidth() + }) + // 全选粘贴无需格式化上下文 + if (~startIndex && !rangeManager.getIsSelectAll()) { + // 如果是复制到虚拟元素里,则粘贴列表的虚拟元素需扁平化处理,避免产生新的虚拟元素 + const anchorElement = elementList[startIndex] + if (anchorElement?.titleId || anchorElement?.listId) { + let start = 0 + while (start < pasteElementList.length) { + const pasteElement = pasteElementList[start] + if (anchorElement.titleId && /^\n/.test(pasteElement.value)) { + break + } + if (VIRTUAL_ELEMENT_TYPE.includes(pasteElement.type!)) { + pasteElementList.splice(start, 1) + if (pasteElement.valueList) { + for (let v = 0; v < pasteElement.valueList.length; v++) { + const element = pasteElement.valueList[v] + if (element.value === ZERO || element.value === '\n') { + continue + } + pasteElementList.splice(start, 0, element) + start++ + } + } + start-- + } + start++ + } + } + formatElementContext(elementList, pasteElementList, startIndex, { + isBreakWhenWrap: true + }) + } + draw.insertElementList(pasteElementList) +} + +export function pasteImage(host: CanvasEvent, file: File | Blob) { + const draw = host.getDraw() + const isReadonly = draw.isReadonly() + if (isReadonly) return + const rangeManager = draw.getRange() + const { startIndex } = rangeManager.getRange() + const elementList = draw.getElementList() + // 创建文件读取器 + const fileReader = new FileReader() + fileReader.readAsDataURL(file) + fileReader.onload = () => { + // 计算宽高 + const image = new Image() + const value = fileReader.result as string + image.src = value + image.onload = () => { + const imageElement: IElement = { + value, + type: ElementType.IMAGE, + width: image.width, + height: image.height + } + if (~startIndex) { + formatElementContext(elementList, [imageElement], startIndex) + } + draw.insertElementList([imageElement]) + } + } +} + +export function pasteByEvent(host: CanvasEvent, evt: ClipboardEvent) { + const draw = host.getDraw() + const isReadonly = draw.isReadonly() + if (isReadonly) return + const clipboardData = evt.clipboardData + if (!clipboardData) return + // 自定义粘贴事件 + const { paste } = draw.getOverride() + if (paste) { + paste(evt) + return + } + // 从粘贴板提取数据 + let isHTML = false + for (let i = 0; i < clipboardData.items.length; i++) { + const item = clipboardData.items[i] + if (item.type === 'text/html') { + isHTML = true + break + } + } + for (let i = 0; i < clipboardData.items.length; i++) { + const item = clipboardData.items[i] + if (item.kind === 'string') { + if (item.type === 'text/plain' && !isHTML) { + item.getAsString(plainText => { + host.input(plainText) + }) + } + if (item.type === 'text/html' && isHTML) { + item.getAsString(htmlText => { + pastHTML(host, htmlText) + }) + } + } else if (item.kind === 'file') { + if (item.type.includes('image')) { + const file = item.getAsFile() + if (file) { + pasteImage(host, file) + } + } + } + } +} + +export async function pasteByApi(host: CanvasEvent, options?: IPasteOption) { + const draw = host.getDraw() + const isReadonly = draw.isReadonly() + if (isReadonly) return + // 自定义粘贴事件 + const { paste } = draw.getOverride() + if (paste) { + paste() + return + } + if (options?.isPlainText) { + const text = await navigator.clipboard.readText() + if (text) { + host.input(text) + } + } else { + const clipboardData = await navigator.clipboard.read() + let isHTML = false + for (const item of clipboardData) { + if (item.types.includes('text/html')) { + isHTML = true + break + } + } + for (const item of clipboardData) { + if (item.types.includes('text/plain') && !isHTML) { + const textBlob = await item.getType('text/plain') + const text = await textBlob.text() + if (text) { + host.input(text) + } + } else if (item.types.includes('text/html') && isHTML) { + const htmlTextBlob = await item.getType('text/html') + const htmlText = await htmlTextBlob.text() + if (htmlText) { + pastHTML(host, htmlText) + } + } else if (item.types.some(type => type.startsWith('image/'))) { + const type = item.types.find(type => type.startsWith('image/'))! + const imageBlob = await item.getType(type) + pasteImage(host, imageBlob) + } + } + } +} diff --git a/src/editor/core/override/Override.ts b/src/editor/core/override/Override.ts index 7147978..1d3969a 100644 --- a/src/editor/core/override/Override.ts +++ b/src/editor/core/override/Override.ts @@ -1,4 +1,4 @@ export class Override { - public paste: ((evt: ClipboardEvent) => void) | undefined + public paste: ((evt?: ClipboardEvent) => void) | undefined public copy: (() => void) | undefined } diff --git a/src/editor/interface/Event.ts b/src/editor/interface/Event.ts new file mode 100644 index 0000000..88a3eee --- /dev/null +++ b/src/editor/interface/Event.ts @@ -0,0 +1,3 @@ +export interface IPasteOption { + isPlainText: boolean +}