diff --git a/src/app/service/content/gm_api/gm_api.ts b/src/app/service/content/gm_api/gm_api.ts index 370e0dc8e..8c955decd 100644 --- a/src/app/service/content/gm_api/gm_api.ts +++ b/src/app/service/content/gm_api/gm_api.ts @@ -1278,7 +1278,19 @@ export default class GMApi extends GM_Base { @GMContext.API({}) GM_setClipboard(data: string, info?: GMTypes.GMClipboardInfo, cb?: () => void) { if (this.isInvalidContext()) return; - this.sendMessage("GM_setClipboard", [data, info]) + // 物件参数意义不明。日后再检视特殊处理 + // 未支持 TM4.19+ application/octet-stream + // 参考: https://github.com/Tampermonkey/tampermonkey/issues/1250 + let mimetype: string | undefined; + if (typeof info === "object" && info?.mimetype) { + mimetype = info.mimetype; + } else { + mimetype = (typeof info === "string" ? info : info?.type) || "text/plain"; + if (mimetype === "text") mimetype = "text/plain"; + else if (mimetype === "html") mimetype = "text/html"; + } + data = `${data}`; // 强制 string type + this.sendMessage("GM_setClipboard", [data, mimetype]) .then(() => { if (typeof cb === "function") { cb(); @@ -1294,7 +1306,11 @@ export default class GMApi extends GM_Base { @GMContext.API({ depend: ["GM_setClipboard"] }) ["GM.setClipboard"](data: string, info?: string | { type?: string; mimetype?: string }): Promise { if (this.isInvalidContext()) return new Promise(() => {}); - return this.sendMessage("GM_setClipboard", [data, info]); + return new Promise((resolve) => { + this.GM_setClipboard(data, info, () => { + resolve(); + }); + }); } @GMContext.API() diff --git a/src/app/service/offscreen/gm_api.ts b/src/app/service/offscreen/gm_api.ts index b9ea11e8a..87943cdc8 100644 --- a/src/app/service/offscreen/gm_api.ts +++ b/src/app/service/offscreen/gm_api.ts @@ -1,5 +1,6 @@ import { BgGMXhr } from "@App/pkg/utils/xhr/bg_gm_xhr"; import type { IGetSender, Group } from "@Packages/message/server"; +import { mightPrepareSetClipboard, setClipboard } from "../service_worker/clipboard"; export default class GMApi { constructor(private group: Group) {} @@ -11,32 +12,12 @@ export default class GMApi { bgGmXhr.do(); } - textarea: HTMLTextAreaElement = document.createElement("textarea"); - - clipboardData: { type?: string; data: string } | undefined; - - async setClipboard({ data, type }: { data: string; type: string }) { - this.clipboardData = { - type, - data, - }; - this.textarea.focus(); - document.execCommand("copy", false, null); + async setClipboard({ data, mimetype }: { data: string; mimetype: string }) { + setClipboard(data, mimetype); } init() { - this.textarea.style.display = "none"; - document.documentElement.appendChild(this.textarea); - document.addEventListener("copy", (e: ClipboardEvent) => { - if (!this.clipboardData || !e.clipboardData) { - return; - } - e.preventDefault(); - const { type, data } = this.clipboardData; - e.clipboardData.setData(type || "text/plain", data); - this.clipboardData = undefined; - }); - + mightPrepareSetClipboard(); this.group.on("xmlHttpRequest", this.xmlHttpRequest.bind(this)); this.group.on("setClipboard", this.setClipboard.bind(this)); } diff --git a/src/app/service/service_worker/clipboard.ts b/src/app/service/service_worker/clipboard.ts new file mode 100644 index 000000000..2948715bc --- /dev/null +++ b/src/app/service/service_worker/clipboard.ts @@ -0,0 +1,39 @@ +let textareaDOM: HTMLTextAreaElement | undefined; +let customClipboardData: { mimetype: string; data: string } | undefined; + +// 抽出成独立处理。日后有需要可以改成 chrome API +export const setClipboard = (data: string, mimetype: string) => { + if (!textareaDOM) { + throw new Error("mightPrepareSetClipboard shall be called first."); + } + customClipboardData = { + mimetype, + data, + }; + textareaDOM!.focus(); + document.execCommand("copy", false, null); +}; + +// 设置 setClipboard 相关DOM +export const mightPrepareSetClipboard = () => { + if (textareaDOM) { + return; + } + if (typeof document !== "object") { + throw new Error( + "mightPrepareSetClipboard shall be only called in either Chrome offscreen or FF background script." + ); + } + textareaDOM = document.createElement("textarea") as HTMLTextAreaElement; + textareaDOM.style.display = "none"; + document.documentElement.appendChild(textareaDOM); + document.addEventListener("copy", (e: ClipboardEvent) => { + if (!customClipboardData || !e?.clipboardData?.setData) { + return; + } + e.preventDefault(); + const { mimetype, data } = customClipboardData; + customClipboardData = undefined; + e.clipboardData.setData(mimetype || "text/plain", data); + }); +}; diff --git a/src/app/service/service_worker/gm_api/gm_api.ts b/src/app/service/service_worker/gm_api/gm_api.ts index 1e85ee677..e59400566 100644 --- a/src/app/service/service_worker/gm_api/gm_api.ts +++ b/src/app/service/service_worker/gm_api/gm_api.ts @@ -42,6 +42,7 @@ import { } from "./gm_xhr"; import { headerModifierMap, headersReceivedMap } from "./gm_xhr"; import { BgGMXhr } from "@App/pkg/utils/xhr/bg_gm_xhr"; +import { mightPrepareSetClipboard, setClipboard } from "../clipboard"; let generatedUniqueMarkerIDs = ""; let generatedUniqueMarkerIDWhen = ""; @@ -1246,10 +1247,15 @@ export default class GMApi { } @PermissionVerify.API() - async GM_setClipboard(request: GMApiRequest<[string, GMTypes.GMClipboardInfo?]>, _sender: IGetSender) { - const [data, type] = request.params; - const clipboardType = type || "text/plain"; - await sendMessage(this.msgSender, "offscreen/gmApi/setClipboard", { data, type: clipboardType }); + async GM_setClipboard(request: GMApiRequest<[string, string]>, _sender: IGetSender) { + const [data, mimetype] = request.params; + if (typeof document === "object" && document?.documentElement) { + // FF background script + mightPrepareSetClipboard(); + setClipboard(data, mimetype); + } else { + await sendMessage(this.msgSender, "offscreen/gmApi/setClipboard", { data, mimetype }); + } } @PermissionVerify.API()