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);
+}