From 4573dc1249eccab92ce83ecb8b3e0864ecacc719 Mon Sep 17 00:00:00 2001 From: Philipinho <16838612+Philipinho@users.noreply.github.com> Date: Mon, 9 Feb 2026 14:33:36 -0800 Subject: [PATCH] support anchor copy --- apps/client/src/hooks/use-clipboard.ts | 17 +++----------- .../editor-ext/src/lib/heading/heading.ts | 3 ++- packages/editor-ext/src/lib/utils.ts | 22 +++++++++++++++++++ 3 files changed, 27 insertions(+), 15 deletions(-) diff --git a/apps/client/src/hooks/use-clipboard.ts b/apps/client/src/hooks/use-clipboard.ts index e05fa56d..e68a92ba 100644 --- a/apps/client/src/hooks/use-clipboard.ts +++ b/apps/client/src/hooks/use-clipboard.ts @@ -1,6 +1,7 @@ // Source: https://github.com/mantinedev/mantine/blob/master/packages/@mantine/hooks/src/use-clipboard/use-clipboard.ts // polyfilled to support execCommand fallback import { useState } from "react"; +import { execCommandCopy } from "@docmost/editor-ext"; export type UseClipboardOptions = { timeout?: number; @@ -33,7 +34,7 @@ export function useClipboard( .then(() => handleCopyResult(true)) .catch(() => { try { - fallbackCopy(value); + execCommandCopy(value); handleCopyResult(true); } catch (err) { setError(err instanceof Error ? err : new Error("Failed to copy")); @@ -41,7 +42,7 @@ export function useClipboard( }); } else { try { - fallbackCopy(value); + execCommandCopy(value); handleCopyResult(true); } catch (err) { setError(err instanceof Error ? err : new Error("Failed to copy")); @@ -57,15 +58,3 @@ export function useClipboard( return { copy, reset, error, copied }; } - -function fallbackCopy(value: string): void { - const textarea = document.createElement("textarea"); - textarea.value = value; - textarea.style.position = "fixed"; - textarea.style.left = "-9999px"; - textarea.style.top = "-9999px"; - document.body.appendChild(textarea); - textarea.select(); - document.execCommand("copy"); - document.body.removeChild(textarea); -} diff --git a/packages/editor-ext/src/lib/heading/heading.ts b/packages/editor-ext/src/lib/heading/heading.ts index 909524fd..8b0df4dd 100644 --- a/packages/editor-ext/src/lib/heading/heading.ts +++ b/packages/editor-ext/src/lib/heading/heading.ts @@ -4,6 +4,7 @@ import TiptapHeading, { import { mergeAttributes } from "@tiptap/react"; import { Decoration, DecorationSet } from "prosemirror-view"; import { Plugin } from "prosemirror-state"; +import { copyToClipboard } from "../utils"; const copyIcon = ``; const successIcon = ``; @@ -41,7 +42,7 @@ export const Heading = TiptapHeading.extend({ const id = node.attrs.id; const baseUrl = window.location.href.split('#')[0]; const url = `${baseUrl}#${id}`; - navigator.clipboard.writeText(url); + copyToClipboard(url); linkBtnContent.innerHTML = successIcon; setTimeout( () => (linkBtnContent.innerHTML = copyIcon), diff --git a/packages/editor-ext/src/lib/utils.ts b/packages/editor-ext/src/lib/utils.ts index 350ab3bb..4c0b798e 100644 --- a/packages/editor-ext/src/lib/utils.ts +++ b/packages/editor-ext/src/lib/utils.ts @@ -384,3 +384,25 @@ export function sanitizeUrl(url: string | undefined): string { const alphabet = "abcdefghijklmnopqrstuvwxyz"; export const generateNodeId = customAlphabet(alphabet, 12); + +export function copyToClipboard(text: string): void { + if ("clipboard" in navigator) { + navigator.clipboard.writeText(text).catch(() => { + execCommandCopy(text); + }); + } else { + execCommandCopy(text); + } +} + +export function execCommandCopy(text: string): void { + const textarea = document.createElement("textarea"); + textarea.value = text; + textarea.style.position = "fixed"; + textarea.style.left = "-9999px"; + textarea.style.top = "-9999px"; + document.body.appendChild(textarea); + textarea.select(); + document.execCommand("copy"); + document.body.removeChild(textarea); +}