mirror of
https://github.com/docmost/docmost.git
synced 2026-05-08 07:13:06 +08:00
WIP
This commit is contained in:
@@ -1,14 +1,14 @@
|
||||
import { MarkViewContent, MarkViewProps } from '@tiptap/react';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { MarkViewContent, MarkViewProps } from "@tiptap/react";
|
||||
import { useNavigate, useLocation } from "react-router-dom";
|
||||
import {
|
||||
IconFileDescription,
|
||||
IconCopy,
|
||||
IconExternalLink,
|
||||
IconLinkOff,
|
||||
} from '@tabler/icons-react';
|
||||
import { useState, useCallback, useRef, useEffect } from 'react';
|
||||
import { useLongPress } from '@/features/editor/hooks/use-long-press';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
} from "@tabler/icons-react";
|
||||
import { useState, useCallback, useRef, useEffect } from "react";
|
||||
import { useLongPress } from "@/features/editor/hooks/use-long-press";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import {
|
||||
Card,
|
||||
Group,
|
||||
@@ -19,15 +19,15 @@ import {
|
||||
Stack,
|
||||
CloseButton,
|
||||
Tooltip,
|
||||
} from '@mantine/core';
|
||||
import classes from './link.module.css';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { INTERNAL_LINK_REGEX } from '@/lib/constants';
|
||||
} from "@mantine/core";
|
||||
import classes from "./link.module.css";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { createPortal } from "react-dom";
|
||||
import { INTERNAL_LINK_REGEX } from "@/lib/constants";
|
||||
|
||||
const isTouchDevice = () => {
|
||||
if (typeof window === 'undefined') return false;
|
||||
return 'ontouchstart' in window || navigator.maxTouchPoints > 0;
|
||||
if (typeof window === "undefined") return false;
|
||||
return "ontouchstart" in window || navigator.maxTouchPoints > 0;
|
||||
};
|
||||
|
||||
const isInternalLink = (href: string): boolean => {
|
||||
@@ -39,20 +39,20 @@ const isInternalLink = (href: string): boolean => {
|
||||
};
|
||||
|
||||
const extractLinkLabel = (href: string): string => {
|
||||
if (!href) return '';
|
||||
if (!href) return "";
|
||||
|
||||
const match = INTERNAL_LINK_REGEX.exec(href);
|
||||
if (match) {
|
||||
const slug = match[5];
|
||||
// Extract page name from slug (remove the ID suffix)
|
||||
const namePart = slug.split('-').slice(0, -1).join('-');
|
||||
const namePart = slug.split("-").slice(0, -1).join("-");
|
||||
return namePart || slug;
|
||||
}
|
||||
|
||||
// For external links, show domain
|
||||
try {
|
||||
const url = new URL(href);
|
||||
return url.hostname.replace('www.', '');
|
||||
return url.hostname.replace("www.", "");
|
||||
} catch {
|
||||
return href.slice(0, 30);
|
||||
}
|
||||
@@ -68,7 +68,7 @@ export default function LinkView(props: MarkViewProps) {
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const [showEditPanel, setShowEditPanel] = useState(false);
|
||||
const [editUrl, setEditUrl] = useState(href);
|
||||
const [editTitle, setEditTitle] = useState('');
|
||||
const [editTitle, setEditTitle] = useState("");
|
||||
const wrapperRef = useRef<HTMLSpanElement>(null);
|
||||
const hoverTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const isTouch = isTouchDevice();
|
||||
@@ -77,13 +77,13 @@ export default function LinkView(props: MarkViewProps) {
|
||||
|
||||
const getLinkText = useCallback(() => {
|
||||
const { state } = editor;
|
||||
let text = '';
|
||||
let text = "";
|
||||
state.doc.descendants((node) => {
|
||||
const linkMark = node.marks.find(
|
||||
(m) => m.type.name === 'link' && m.attrs.href === href
|
||||
(m) => m.type.name === "link" && m.attrs.href === href,
|
||||
);
|
||||
if (linkMark && node.isText) {
|
||||
text = node.text || '';
|
||||
text = node.text || "";
|
||||
return false;
|
||||
}
|
||||
});
|
||||
@@ -119,7 +119,7 @@ export default function LinkView(props: MarkViewProps) {
|
||||
if (isInternal) {
|
||||
// Get pathname for navigation (handle both relative and absolute URLs)
|
||||
let targetPath = href;
|
||||
let anchor = '';
|
||||
let anchor = "";
|
||||
|
||||
try {
|
||||
const url = new URL(href);
|
||||
@@ -127,8 +127,8 @@ export default function LinkView(props: MarkViewProps) {
|
||||
anchor = url.hash.slice(1); // Remove the # prefix
|
||||
} catch {
|
||||
// Relative URL
|
||||
if (href.includes('#')) {
|
||||
[targetPath, anchor] = href.split('#');
|
||||
if (href.includes("#")) {
|
||||
[targetPath, anchor] = href.split("#");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,7 +138,7 @@ export default function LinkView(props: MarkViewProps) {
|
||||
if (!targetPath || targetPath === currentPath) {
|
||||
const element = document.getElementById(anchor);
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
element.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
navigate(`${currentPath}#${anchor}`, { replace: true });
|
||||
return;
|
||||
}
|
||||
@@ -147,7 +147,7 @@ export default function LinkView(props: MarkViewProps) {
|
||||
|
||||
navigate(anchor ? `${targetPath}#${anchor}` : targetPath);
|
||||
} else {
|
||||
window.open(href, '_blank', 'noopener,noreferrer');
|
||||
window.open(href, "_blank", "noopener,noreferrer");
|
||||
}
|
||||
}, [href, navigate, location.pathname, isInternal]);
|
||||
|
||||
@@ -159,7 +159,7 @@ export default function LinkView(props: MarkViewProps) {
|
||||
handleNavigate();
|
||||
}
|
||||
},
|
||||
[handleNavigate, showEditPanel]
|
||||
[handleNavigate, showEditPanel],
|
||||
);
|
||||
|
||||
const handleLongPress = useCallback(() => {
|
||||
@@ -176,7 +176,7 @@ export default function LinkView(props: MarkViewProps) {
|
||||
handleNavigate();
|
||||
}
|
||||
},
|
||||
[handleNavigate, showEditPanel]
|
||||
[handleNavigate, showEditPanel],
|
||||
);
|
||||
|
||||
const longPressHandlers = useLongPress({
|
||||
@@ -204,12 +204,12 @@ export default function LinkView(props: MarkViewProps) {
|
||||
const fullUrl = isInternal ? `${window.location.origin}${href}` : href;
|
||||
navigator.clipboard.writeText(fullUrl);
|
||||
notifications.show({
|
||||
message: t('Link copied to clipboard'),
|
||||
color: 'green',
|
||||
message: t("Link copied to clipboard"),
|
||||
color: "green",
|
||||
autoClose: 2000,
|
||||
});
|
||||
},
|
||||
[href, isInternal, t]
|
||||
[href, isInternal, t],
|
||||
);
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
@@ -221,7 +221,7 @@ export default function LinkView(props: MarkViewProps) {
|
||||
if (updated) return false;
|
||||
|
||||
const linkMark = node.marks.find(
|
||||
(m) => m.type.name === 'link' && m.attrs.href === href
|
||||
(m) => m.type.name === "link" && m.attrs.href === href,
|
||||
);
|
||||
if (linkMark && node.isText) {
|
||||
const from = pos;
|
||||
@@ -232,14 +232,14 @@ export default function LinkView(props: MarkViewProps) {
|
||||
tr.addMark(from, to, linkMark.type.create({ href: editUrl }));
|
||||
}
|
||||
|
||||
const currentText = node.text || '';
|
||||
const currentText = node.text || "";
|
||||
if (editTitle && editTitle !== currentText) {
|
||||
tr.replaceWith(
|
||||
from,
|
||||
to,
|
||||
state.schema.text(editTitle, [
|
||||
linkMark.type.create({ href: editUrl || href }),
|
||||
])
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -256,7 +256,7 @@ export default function LinkView(props: MarkViewProps) {
|
||||
}, [editor, href, editUrl, editTitle]);
|
||||
|
||||
const handleRemoveLink = useCallback(() => {
|
||||
editor.chain().focus().extendMarkRange('link').unsetLink().run();
|
||||
editor.chain().focus().extendMarkRange("link").unsetLink().run();
|
||||
setShowEditPanel(false);
|
||||
}, [editor]);
|
||||
|
||||
@@ -265,15 +265,15 @@ export default function LinkView(props: MarkViewProps) {
|
||||
// Stop all keyboard events from bubbling to TipTap editor
|
||||
e.stopPropagation();
|
||||
|
||||
if (e.key === 'Enter') {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
handleSave();
|
||||
} else if (e.key === 'Escape') {
|
||||
} else if (e.key === "Escape") {
|
||||
e.preventDefault();
|
||||
handleCloseEdit();
|
||||
}
|
||||
},
|
||||
[handleSave, handleCloseEdit]
|
||||
[handleSave, handleCloseEdit],
|
||||
);
|
||||
|
||||
const interactionProps = isTouch
|
||||
@@ -297,8 +297,8 @@ export default function LinkView(props: MarkViewProps) {
|
||||
href={href}
|
||||
className={classes.linkText}
|
||||
onClick={(e) => e.preventDefault()}
|
||||
target={isInternal ? undefined : '_blank'}
|
||||
rel={isInternal ? undefined : 'noopener noreferrer'}
|
||||
target={isInternal ? undefined : "_blank"}
|
||||
rel={isInternal ? undefined : "noopener noreferrer"}
|
||||
>
|
||||
<MarkViewContent />
|
||||
</a>
|
||||
@@ -316,7 +316,7 @@ export default function LinkView(props: MarkViewProps) {
|
||||
<Group
|
||||
gap={6}
|
||||
wrap="nowrap"
|
||||
style={{ cursor: 'pointer', maxWidth: 180 }}
|
||||
style={{ cursor: "pointer", maxWidth: 180 }}
|
||||
onClick={handleNavigate}
|
||||
>
|
||||
{isInternal ? (
|
||||
@@ -329,20 +329,28 @@ export default function LinkView(props: MarkViewProps) {
|
||||
</Text>
|
||||
</Group>
|
||||
|
||||
<Tooltip label={t('Copy link')} withArrow>
|
||||
<ActionIcon variant="subtle" color="gray" onClick={handleCopy}>
|
||||
<Tooltip label={t("Copy link")} withArrow>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
onClick={handleCopy}
|
||||
>
|
||||
<IconCopy size={18} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip label={t('Remove link')} withArrow>
|
||||
<ActionIcon variant="subtle" color="gray" onClick={handleRemoveLink}>
|
||||
<Tooltip label={t("Remove link")} withArrow>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
onClick={handleRemoveLink}
|
||||
>
|
||||
<IconLinkOff size={18} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<Button size="xs" variant="subtle" onClick={handleOpenEdit}>
|
||||
{t('Edit')}
|
||||
{t("Edit")}
|
||||
</Button>
|
||||
</Group>
|
||||
</Card>
|
||||
@@ -357,7 +365,7 @@ export default function LinkView(props: MarkViewProps) {
|
||||
className={classes.editPanelOverlay}
|
||||
onClick={handleCloseEdit}
|
||||
/>,
|
||||
document.body
|
||||
document.body,
|
||||
)}
|
||||
<div
|
||||
contentEditable={false}
|
||||
@@ -369,14 +377,14 @@ export default function LinkView(props: MarkViewProps) {
|
||||
<Card shadow="md" padding="md" radius="md" withBorder w={320}>
|
||||
<Stack gap="md">
|
||||
<TextInput
|
||||
label={t('Search or paste a link')}
|
||||
label={t("Search or paste a link")}
|
||||
placeholder="https://..."
|
||||
value={editUrl}
|
||||
onChange={(e) => setEditUrl(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
rightSection={
|
||||
editUrl && (
|
||||
<CloseButton size="sm" onClick={() => setEditUrl('')} />
|
||||
<CloseButton size="sm" onClick={() => setEditUrl("")} />
|
||||
)
|
||||
}
|
||||
autoFocus
|
||||
@@ -384,9 +392,9 @@ export default function LinkView(props: MarkViewProps) {
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label={t('Display text (optional)')}
|
||||
description={t('Give this link a title or description')}
|
||||
placeholder={t('Text to display')}
|
||||
label={t("Display text (optional)")}
|
||||
description={t("Give this link a title or description")}
|
||||
placeholder={t("Text to display")}
|
||||
value={editTitle}
|
||||
onChange={(e) => setEditTitle(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
@@ -398,10 +406,10 @@ export default function LinkView(props: MarkViewProps) {
|
||||
onClick={handleCloseEdit}
|
||||
size="sm"
|
||||
>
|
||||
{t('Cancel')}
|
||||
{t("Cancel")}
|
||||
</Button>
|
||||
<Button onClick={handleSave} size="sm">
|
||||
{t('Save')}
|
||||
{t("Save")}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
|
||||
@@ -42,7 +42,7 @@ export const useEditorScroll = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const dom = editor.view.dom.querySelector(`[id="${targetId}"]`);
|
||||
const dom = editor.view.dom.querySelector(`[id="${targetId}"], [data-id="${targetId}"]`);
|
||||
if (dom) {
|
||||
dom.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
resolve(true);
|
||||
|
||||
Reference in New Issue
Block a user