From 280f97ee812ad86f7fefcb2f3c8dfc7288002296 Mon Sep 17 00:00:00 2001 From: Jeffrey Chen <78434827+TCOTC@users.noreply.github.com> Date: Thu, 17 Jul 2025 16:49:46 +0800 Subject: [PATCH] Improve right-click menu paste (#15286) * :art: Improve right-click menu paste * :art: Improve right-click menu paste --- app/src/protyle/util/compatibility.ts | 31 ++++++++++++++++++++++++++- app/src/protyle/util/paste.ts | 2 ++ app/src/protyle/wysiwyg/index.ts | 24 ++++++++++++++++----- 3 files changed, 51 insertions(+), 6 deletions(-) diff --git a/app/src/protyle/util/compatibility.ts b/app/src/protyle/util/compatibility.ts index b4de41560..0c0fc11ef 100644 --- a/app/src/protyle/util/compatibility.ts +++ b/app/src/protyle/util/compatibility.ts @@ -2,6 +2,22 @@ import {focusByRange} from "./selection"; import {fetchPost} from "../../util/fetch"; import {Constants} from "../../constants"; +export const encodeBase64 = (text: string): string => { + const encoder = new TextEncoder(); + const bytes = encoder.encode(text); + const binaryString = String.fromCharCode(...bytes); + const base64 = btoa(binaryString); + return base64; +}; + +export const decodeBase64 = (base64: string): string => { + const decoder = new TextDecoder(); + const binaryString = atob(base64); + const bytes = Uint8Array.from(binaryString, char => char.charCodeAt(0)); + const text = decoder.decode(bytes); + return text; +}; + export const openByMobile = (uri: string) => { if (!uri) { return; @@ -58,14 +74,27 @@ export const readClipboard = async () => { const text: { textHTML?: string, textPlain?: string, + siyuanHTML?: string, files?: File[], - } = {textPlain: "", textHTML: ""}; + } = {textPlain: "", textHTML: "", siyuanHTML: ""}; try { const clipboardContents = await navigator.clipboard.read(); for (const item of clipboardContents) { if (item.types.includes("text/html")) { const blob = await item.getType("text/html"); text.textHTML = await blob.text(); + + // 从 text/html 中的注释节点提取 text/siyuan 数据 + const siyuanMatch = text.textHTML.match(//); + if (siyuanMatch) { + try { + text.siyuanHTML = decodeBase64(siyuanMatch[1]); + // 移除注释节点,保持原有的 text/html 内容 + text.textHTML = text.textHTML.replace(//, ""); + } catch (e) { + console.log("Failed to decode siyuan data from HTML comment:", e); + } + } } if (item.types.includes("text/plain")) { const blob = await item.getType("text/plain"); diff --git a/app/src/protyle/util/paste.ts b/app/src/protyle/util/paste.ts index 6745b33d7..3959dc20a 100644 --- a/app/src/protyle/util/paste.ts +++ b/app/src/protyle/util/paste.ts @@ -248,6 +248,7 @@ const readLocalFile = async (protyle: IProtyle, localFiles: string[]) => { export const paste = async (protyle: IProtyle, event: (ClipboardEvent | DragEvent | { textHTML?: string, textPlain?: string, + siyuanHTML?: string, files?: File[], }) & { target: HTMLElement @@ -275,6 +276,7 @@ export const paste = async (protyle: IProtyle, event: (ClipboardEvent | DragEven } else { textHTML = event.textHTML; textPlain = event.textPlain; + siyuanHTML = event.siyuanHTML; files = event.files; } diff --git a/app/src/protyle/wysiwyg/index.ts b/app/src/protyle/wysiwyg/index.ts index 980fbe297..489a26111 100644 --- a/app/src/protyle/wysiwyg/index.ts +++ b/app/src/protyle/wysiwyg/index.ts @@ -66,7 +66,7 @@ import {openGlobalSearch} from "../../search/util"; import {popSearch} from "../../mobile/menu/search"; /// #endif import {BlockPanel} from "../../block/Panel"; -import {copyPlainText, isInIOS, isMac, isOnlyMeta, readClipboard} from "../util/compatibility"; +import {copyPlainText, isInIOS, isMac, isOnlyMeta, readClipboard, encodeBase64} from "../util/compatibility"; import {MenuItem} from "../../menus/Menu"; import {fetchPost} from "../../util/fetch"; import {onGet} from "../util/onGet"; @@ -442,10 +442,17 @@ export class WYSIWYG { // Remove ZWSP when copying inline elements https://github.com/siyuan-note/siyuan/issues/13882 .replace(new RegExp(Constants.ZWSP, "g"), ""); event.clipboardData.setData("text/plain", textPlain); - event.clipboardData.setData("text/html", selectTableElement ? html : protyle.lute.BlockDOM2HTML(selectAVElement ? textPlain : html)); + + // 获取 text/siyuan 数据 enableLuteMarkdownSyntax(protyle); - event.clipboardData.setData("text/siyuan", selectTableElement ? protyle.lute.HTML2BlockDOM(html) : html); + const siyuanData = selectTableElement ? protyle.lute.HTML2BlockDOM(html) : html; + event.clipboardData.setData("text/siyuan", siyuanData); restoreLuteMarkdownSyntax(protyle); + + // 在 text/html 中插入注释节点,用于右键菜单粘贴时获取 text/siyuan 数据 + const textHTML = selectTableElement ? html : protyle.lute.BlockDOM2HTML(selectAVElement ? textPlain : html); + const siyuanComment = ``; + event.clipboardData.setData("text/html", siyuanComment + textHTML); }); this.element.addEventListener("mousedown", (event: MouseEvent) => { @@ -1940,10 +1947,17 @@ export class WYSIWYG { } textPlain = textPlain.replace(/\u00A0/g, " "); // Replace non-breaking spaces with normal spaces when copying https://github.com/siyuan-note/siyuan/issues/9382 event.clipboardData.setData("text/plain", textPlain); - event.clipboardData.setData("text/html", selectTableElement ? html : protyle.lute.BlockDOM2HTML(selectAVElement ? textPlain : html)); + + // 获取 text/siyuan 数据 enableLuteMarkdownSyntax(protyle); - event.clipboardData.setData("text/siyuan", selectTableElement ? protyle.lute.HTML2BlockDOM(html) : html); + const siyuanData = selectTableElement ? protyle.lute.HTML2BlockDOM(html) : html; + event.clipboardData.setData("text/siyuan", siyuanData); restoreLuteMarkdownSyntax(protyle); + + // 在 text/html 中插入注释节点,用于右键菜单粘贴时获取 text/siyuan 数据 + const textHTML = selectTableElement ? html : protyle.lute.BlockDOM2HTML(selectAVElement ? textPlain : html); + const siyuanComment = ``; + event.clipboardData.setData("text/html", siyuanComment + textHTML); }); let beforeContextmenuRange: Range;