mirror of
https://github.com/docmost/docmost.git
synced 2026-05-07 14:43:06 +08:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4573dc1249 | |||
| f7a9004c73 |
@@ -0,0 +1,33 @@
|
|||||||
|
// Source: https://github.com/mantinedev/mantine/blob/master/packages/@mantine/core/src/components/CopyButton/CopyButton.tsx - MIT
|
||||||
|
// modified to use the polyfilled clipboard api
|
||||||
|
import React from "react";
|
||||||
|
import { useClipboard } from "@/hooks/use-clipboard";
|
||||||
|
import { useProps } from "@mantine/core";
|
||||||
|
|
||||||
|
interface CopyButtonProps {
|
||||||
|
/** Children callback, provides current status and copy function as an argument */
|
||||||
|
children: (payload: { copied: boolean; copy: () => void }) => React.ReactNode;
|
||||||
|
|
||||||
|
/** Value that is copied to the clipboard when the button is clicked */
|
||||||
|
value: string;
|
||||||
|
|
||||||
|
/** Copied status timeout in ms @default `1000` */
|
||||||
|
timeout?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultProps = {
|
||||||
|
timeout: 1000,
|
||||||
|
} satisfies Partial<CopyButtonProps>;
|
||||||
|
|
||||||
|
export function CopyButton(props: CopyButtonProps) {
|
||||||
|
const { children, timeout, value, ...others } = useProps(
|
||||||
|
"CopyButton",
|
||||||
|
defaultProps,
|
||||||
|
props,
|
||||||
|
);
|
||||||
|
const clipboard = useClipboard({ timeout });
|
||||||
|
const copy = () => clipboard.copy(value);
|
||||||
|
return <>{children({ copy, copied: clipboard.copied, ...others })}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
CopyButton.displayName = "@mantine/core/CopyButton";
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { ActionIcon, CopyButton, Tooltip } from "@mantine/core";
|
import { ActionIcon, Tooltip } from "@mantine/core";
|
||||||
|
import { CopyButton } from "@/components/common/copy-button";
|
||||||
import { IconCheck, IconCopy } from "@tabler/icons-react";
|
import { IconCheck, IconCopy } from "@tabler/icons-react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ import {
|
|||||||
Group,
|
Group,
|
||||||
List,
|
List,
|
||||||
Code,
|
Code,
|
||||||
CopyButton,
|
|
||||||
Alert,
|
Alert,
|
||||||
PasswordInput,
|
PasswordInput,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
|
import { CopyButton } from "@/components/common/copy-button";
|
||||||
import {
|
import {
|
||||||
IconRefresh,
|
IconRefresh,
|
||||||
IconCopy,
|
IconCopy,
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
PinInput,
|
PinInput,
|
||||||
Alert,
|
Alert,
|
||||||
List,
|
List,
|
||||||
CopyButton,
|
|
||||||
ActionIcon,
|
ActionIcon,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Paper,
|
Paper,
|
||||||
@@ -20,6 +19,7 @@ import {
|
|||||||
Collapse,
|
Collapse,
|
||||||
UnstyledButton,
|
UnstyledButton,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
|
import { CopyButton } from "@/components/common/copy-button";
|
||||||
import {
|
import {
|
||||||
IconQrcode,
|
IconQrcode,
|
||||||
IconShieldCheck,
|
IconShieldCheck,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { NodeViewContent, NodeViewProps, NodeViewWrapper } from "@tiptap/react";
|
import { NodeViewContent, NodeViewProps, NodeViewWrapper } from "@tiptap/react";
|
||||||
import { ActionIcon, CopyButton, Group, Select, Tooltip } from "@mantine/core";
|
import { ActionIcon, Group, Select, Tooltip } from "@mantine/core";
|
||||||
|
import { CopyButton } from "@/components/common/copy-button";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { IconCheck, IconCopy } from "@tabler/icons-react";
|
import { IconCheck, IconCopy } from "@tabler/icons-react";
|
||||||
import classes from "./code-block.module.css";
|
import classes from "./code-block.module.css";
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ import React, { useEffect, useRef, useState } from "react";
|
|||||||
import useToggleAside from "@/hooks/use-toggle-aside.tsx";
|
import useToggleAside from "@/hooks/use-toggle-aside.tsx";
|
||||||
import { useAtom, useAtomValue } from "jotai";
|
import { useAtom, useAtomValue } from "jotai";
|
||||||
import { historyAtoms } from "@/features/page-history/atoms/history-atoms.ts";
|
import { historyAtoms } from "@/features/page-history/atoms/history-atoms.ts";
|
||||||
import { useClipboard, useDisclosure, useHotkeys } from "@mantine/hooks";
|
import { useDisclosure, useHotkeys } from "@mantine/hooks";
|
||||||
|
import { useClipboard } from "@/hooks/use-clipboard";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import { usePageQuery } from "@/features/page/queries/page-query.ts";
|
import { usePageQuery } from "@/features/page/queries/page-query.ts";
|
||||||
import { buildPageUrl } from "@/features/page/page.utils.ts";
|
import { buildPageUrl } from "@/features/page/page.utils.ts";
|
||||||
|
|||||||
@@ -54,11 +54,11 @@ import { IPage, SidebarPagesParams } from "@/features/page/types/page.types.ts";
|
|||||||
import { queryClient } from "@/main.tsx";
|
import { queryClient } from "@/main.tsx";
|
||||||
import { OpenMap } from "react-arborist/dist/main/state/open-slice";
|
import { OpenMap } from "react-arborist/dist/main/state/open-slice";
|
||||||
import {
|
import {
|
||||||
useClipboard,
|
|
||||||
useDisclosure,
|
useDisclosure,
|
||||||
useElementSize,
|
useElementSize,
|
||||||
useMergedRef,
|
useMergedRef,
|
||||||
} from "@mantine/hooks";
|
} from "@mantine/hooks";
|
||||||
|
import { useClipboard } from "@/hooks/use-clipboard";
|
||||||
import { dfs } from "react-arborist/dist/module/utils";
|
import { dfs } from "react-arborist/dist/module/utils";
|
||||||
import { useQueryEmit } from "@/features/websocket/use-query-emit.ts";
|
import { useQueryEmit } from "@/features/websocket/use-query-emit.ts";
|
||||||
import { buildPageUrl } from "@/features/page/page.utils.ts";
|
import { buildPageUrl } from "@/features/page/page.utils.ts";
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
buildPageUrl,
|
buildPageUrl,
|
||||||
buildSharedPageUrl,
|
buildSharedPageUrl,
|
||||||
} from "@/features/page/page.utils.ts";
|
} from "@/features/page/page.utils.ts";
|
||||||
import { useClipboard } from "@mantine/hooks";
|
import { useClipboard } from "@/hooks/use-clipboard";
|
||||||
import { notifications } from "@mantine/notifications";
|
import { notifications } from "@mantine/notifications";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { useDeleteShareMutation } from "@/features/share/queries/share-query.ts";
|
import { useDeleteShareMutation } from "@/features/share/queries/share-query.ts";
|
||||||
|
|||||||
+1
-1
@@ -8,7 +8,7 @@ import {
|
|||||||
} from "@/features/workspace/queries/workspace-query.ts";
|
} from "@/features/workspace/queries/workspace-query.ts";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { notifications } from "@mantine/notifications";
|
import { notifications } from "@mantine/notifications";
|
||||||
import { useClipboard } from "@mantine/hooks";
|
import { useClipboard } from "@/hooks/use-clipboard";
|
||||||
import { getInviteLink } from "@/features/workspace/services/workspace-service.ts";
|
import { getInviteLink } from "@/features/workspace/services/workspace-service.ts";
|
||||||
import useUserRole from "@/hooks/use-user-role.tsx";
|
import useUserRole from "@/hooks/use-user-role.tsx";
|
||||||
import { isCloud } from "@/lib/config.ts";
|
import { isCloud } from "@/lib/config.ts";
|
||||||
|
|||||||
+2
-1
@@ -1,7 +1,8 @@
|
|||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import { currentUserAtom } from "@/features/user/atoms/current-user-atom.ts";
|
import { currentUserAtom } from "@/features/user/atoms/current-user-atom.ts";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Button, CopyButton, Group, Text, TextInput } from "@mantine/core";
|
import { Button, Group, Text, TextInput } from "@mantine/core";
|
||||||
|
import { CopyButton } from "@/components/common/copy-button";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export default function WorkspaceInviteSection() {
|
export default function WorkspaceInviteSection() {
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
// 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;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UseClipboardReturnValue = {
|
||||||
|
copy: (value: string) => void;
|
||||||
|
reset: () => void;
|
||||||
|
error: Error | null;
|
||||||
|
copied: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useClipboard(
|
||||||
|
options: UseClipboardOptions = { timeout: 2000 },
|
||||||
|
): UseClipboardReturnValue {
|
||||||
|
const [error, setError] = useState<Error | null>(null);
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
const [copyTimeout, setCopyTimeout] = useState<number | null>(null);
|
||||||
|
|
||||||
|
const handleCopyResult = (value: boolean) => {
|
||||||
|
window.clearTimeout(copyTimeout!);
|
||||||
|
setCopyTimeout(window.setTimeout(() => setCopied(false), options.timeout));
|
||||||
|
setCopied(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const copy = (value: string) => {
|
||||||
|
if ("clipboard" in navigator) {
|
||||||
|
navigator.clipboard
|
||||||
|
.writeText(value)
|
||||||
|
.then(() => handleCopyResult(true))
|
||||||
|
.catch(() => {
|
||||||
|
try {
|
||||||
|
execCommandCopy(value);
|
||||||
|
handleCopyResult(true);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err : new Error("Failed to copy"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
execCommandCopy(value);
|
||||||
|
handleCopyResult(true);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err : new Error("Failed to copy"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const reset = () => {
|
||||||
|
setCopied(false);
|
||||||
|
setError(null);
|
||||||
|
window.clearTimeout(copyTimeout!);
|
||||||
|
};
|
||||||
|
|
||||||
|
return { copy, reset, error, copied };
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import TiptapHeading, {
|
|||||||
import { mergeAttributes } from "@tiptap/react";
|
import { mergeAttributes } from "@tiptap/react";
|
||||||
import { Decoration, DecorationSet } from "prosemirror-view";
|
import { Decoration, DecorationSet } from "prosemirror-view";
|
||||||
import { Plugin } from "prosemirror-state";
|
import { Plugin } from "prosemirror-state";
|
||||||
|
import { copyToClipboard } from "../utils";
|
||||||
|
|
||||||
const copyIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24"><!-- Icon from Material Symbols Light by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="currentColor" d="M10.616 16.077H7.077q-1.692 0-2.884-1.192T3 12t1.193-2.885t2.884-1.193h3.539v1H7.077q-1.27 0-2.173.904Q4 10.731 4 12t.904 2.173t2.173.904h3.539zM8.5 12.5v-1h7v1zm4.885 3.577v-1h3.538q1.27 0 2.173-.904Q20 13.269 20 12t-.904-2.173t-2.173-.904h-3.538v-1h3.538q1.692 0 2.885 1.192T21 12t-1.193 2.885t-2.884 1.193z"/></svg>`;
|
const copyIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24"><!-- Icon from Material Symbols Light by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="currentColor" d="M10.616 16.077H7.077q-1.692 0-2.884-1.192T3 12t1.193-2.885t2.884-1.193h3.539v1H7.077q-1.27 0-2.173.904Q4 10.731 4 12t.904 2.173t2.173.904h3.539zM8.5 12.5v-1h7v1zm4.885 3.577v-1h3.538q1.27 0 2.173-.904Q20 13.269 20 12t-.904-2.173t-2.173-.904h-3.538v-1h3.538q1.692 0 2.885 1.192T21 12t-1.193 2.885t-2.884 1.193z"/></svg>`;
|
||||||
const successIcon = `<svg xmlns="http://www.w3.org/2000/svg" style="color: forestgreen;" width="18" height="18" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="currentColor" d="m10.6 16.6l7.05-7.05l-1.4-1.4l-5.65 5.65l-2.85-2.85l-1.4 1.4zM12 22q-2.075 0-3.9-.788t-3.175-2.137T2.788 15.9T2 12t.788-3.9t2.137-3.175T8.1 2.788T12 2t3.9.788t3.175 2.137T21.213 8.1T22 12t-.788 3.9t-2.137 3.175t-3.175 2.138T12 22"/></svg>`;
|
const successIcon = `<svg xmlns="http://www.w3.org/2000/svg" style="color: forestgreen;" width="18" height="18" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="currentColor" d="m10.6 16.6l7.05-7.05l-1.4-1.4l-5.65 5.65l-2.85-2.85l-1.4 1.4zM12 22q-2.075 0-3.9-.788t-3.175-2.137T2.788 15.9T2 12t.788-3.9t2.137-3.175T8.1 2.788T12 2t3.9.788t3.175 2.137T21.213 8.1T22 12t-.788 3.9t-2.137 3.175t-3.175 2.138T12 22"/></svg>`;
|
||||||
@@ -41,7 +42,7 @@ export const Heading = TiptapHeading.extend<TiptapHeadingOptions>({
|
|||||||
const id = node.attrs.id;
|
const id = node.attrs.id;
|
||||||
const baseUrl = window.location.href.split('#')[0];
|
const baseUrl = window.location.href.split('#')[0];
|
||||||
const url = `${baseUrl}#${id}`;
|
const url = `${baseUrl}#${id}`;
|
||||||
navigator.clipboard.writeText(url);
|
copyToClipboard(url);
|
||||||
linkBtnContent.innerHTML = successIcon;
|
linkBtnContent.innerHTML = successIcon;
|
||||||
setTimeout(
|
setTimeout(
|
||||||
() => (linkBtnContent.innerHTML = copyIcon),
|
() => (linkBtnContent.innerHTML = copyIcon),
|
||||||
|
|||||||
@@ -384,3 +384,25 @@ export function sanitizeUrl(url: string | undefined): string {
|
|||||||
|
|
||||||
const alphabet = "abcdefghijklmnopqrstuvwxyz";
|
const alphabet = "abcdefghijklmnopqrstuvwxyz";
|
||||||
export const generateNodeId = customAlphabet(alphabet, 12);
|
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);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user