mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-08-29 10:09:26 +02:00
Merge pull request #9506 from streetwriters/editor/internal-link-title-in-popup
editor: show internal link's title in hover popup
This commit is contained in:
@@ -20,7 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
/* eslint-disable no-case-declarations */
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
import { isFeatureAvailable, useAreFeaturesAvailable } from "@notesnook/common";
|
||||
import { ItemReference } from "@notesnook/core";
|
||||
import { ItemReference, parseInternalLink } from "@notesnook/core";
|
||||
import type { Attachment } from "@notesnook/editor";
|
||||
import { EditorEvents } from "@notesnook/editor-mobile/src/utils/editor-events";
|
||||
import { NativeEvents } from "@notesnook/editor-mobile/src/utils/native-events";
|
||||
@@ -471,9 +471,8 @@ export const useEditorEvents = (
|
||||
relationType: "from",
|
||||
title: strings.dataTypesPluralCamelCase.reminder(),
|
||||
onAdd: async () => {
|
||||
const reminderFeature = await isFeatureAvailable(
|
||||
"activeReminders"
|
||||
);
|
||||
const reminderFeature =
|
||||
await isFeatureAvailable("activeReminders");
|
||||
if (!reminderFeature.isAllowed) {
|
||||
ToastManager.show({
|
||||
type: "info",
|
||||
@@ -533,7 +532,59 @@ export const useEditorEvents = (
|
||||
downloadAttachment((editorMessage.value as Attachment)?.hash, true);
|
||||
break;
|
||||
}
|
||||
case EditorEvents.getLinkData: {
|
||||
const url = (editorMessage.value as any)?.url as string;
|
||||
const link = parseInternalLink(url);
|
||||
if (!link) return;
|
||||
switch (link.type) {
|
||||
case "note":
|
||||
case "notebook":
|
||||
case "tag": {
|
||||
const table =
|
||||
link.type === "note"
|
||||
? "notes"
|
||||
: link.type === "notebook"
|
||||
? "notebooks"
|
||||
: "tags";
|
||||
const item = await db
|
||||
.sql()
|
||||
.selectFrom(table)
|
||||
.where("id", "=", link.id)
|
||||
.select("title")
|
||||
.executeTakeFirst();
|
||||
|
||||
editor.postMessage(NativeEvents.resolve, {
|
||||
resolverId: editorMessage.resolverId,
|
||||
data: {
|
||||
type: link.type,
|
||||
title: item?.title || ""
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "color": {
|
||||
const color = await db
|
||||
.sql()
|
||||
.selectFrom("colors")
|
||||
.where("id", "=", link.id)
|
||||
.select(["title", "colorCode"])
|
||||
.executeTakeFirst();
|
||||
|
||||
editor.postMessage(NativeEvents.resolve, {
|
||||
resolverId: editorMessage.resolverId,
|
||||
data: {
|
||||
type: "color",
|
||||
title: color?.title || "",
|
||||
metadata: {
|
||||
colorCode: color?.colorCode || ""
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case EditorEvents.getAttachmentData: {
|
||||
const data = (editorMessage.value as any)?.attachment as Attachment;
|
||||
|
||||
|
||||
4
apps/mobile/package-lock.json
generated
4
apps/mobile/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@notesnook/mobile",
|
||||
"version": "3.3.17",
|
||||
"version": "3.3.18",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@notesnook/mobile",
|
||||
"version": "3.3.17",
|
||||
"version": "3.3.18",
|
||||
"hasInstallScript": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
|
||||
@@ -69,6 +69,7 @@ import { UpgradeDialog } from "../../dialogs/buy-dialog/upgrade-dialog";
|
||||
import { ConfirmDialog } from "../../dialogs/confirm";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { handleInternalLink } from "../../common";
|
||||
import { db } from "../../common/db";
|
||||
|
||||
export type OnChangeHandler = (
|
||||
content: () => string,
|
||||
@@ -426,6 +427,48 @@ function TipTap(props: TipTapProps) {
|
||||
const link = parseInternalLink(url);
|
||||
if (link) handleInternalLink(url, openInNewTab);
|
||||
else window.open(url, "_blank");
|
||||
},
|
||||
getLinkData: async (url) => {
|
||||
const link = parseInternalLink(url);
|
||||
if (!link) return;
|
||||
|
||||
switch (link.type) {
|
||||
case "note":
|
||||
case "notebook":
|
||||
case "tag": {
|
||||
const table =
|
||||
link.type === "note"
|
||||
? "notes"
|
||||
: link.type === "notebook"
|
||||
? "notebooks"
|
||||
: "tags";
|
||||
const item = await db
|
||||
.sql()
|
||||
.selectFrom(table)
|
||||
.where("id", "=", link.id)
|
||||
.select("title")
|
||||
.executeTakeFirst();
|
||||
return {
|
||||
type: link.type,
|
||||
title: item?.title || ""
|
||||
};
|
||||
}
|
||||
case "color": {
|
||||
const color = await db
|
||||
.sql()
|
||||
.selectFrom("colors")
|
||||
.where("id", "=", link.id)
|
||||
.select(["title", "colorCode"])
|
||||
.executeTakeFirst();
|
||||
return {
|
||||
type: "color",
|
||||
title: color?.title || "",
|
||||
metadata: {
|
||||
colorCode: color?.colorCode || ""
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}, [
|
||||
|
||||
2
packages/editor-mobile/package-lock.json
generated
2
packages/editor-mobile/package-lock.json
generated
@@ -68,7 +68,7 @@
|
||||
"@notesnook/intl": "file:../intl",
|
||||
"@notesnook/theme": "file:../theme",
|
||||
"@notesnook/ui": "file:../ui",
|
||||
"@social-embed/lib": "^0.1.0-next.7",
|
||||
"@social-embed/lib": "^0.1.0-next.11",
|
||||
"@tiptap/core": "2.6.6",
|
||||
"@tiptap/extension-blockquote": "^2.6.6",
|
||||
"@tiptap/extension-bullet-list": "^2.6.6",
|
||||
|
||||
@@ -162,6 +162,11 @@ const Tiptap = ({
|
||||
attachment
|
||||
) as Promise<string | undefined>;
|
||||
},
|
||||
getLinkData: (url: string) => {
|
||||
return postAsyncWithTimeout(EditorEvents.getLinkData, {
|
||||
url: url
|
||||
});
|
||||
},
|
||||
createInternalLink(attributes) {
|
||||
return postAsyncWithTimeout(EditorEvents.createInternalLink, {
|
||||
attributes
|
||||
|
||||
@@ -55,5 +55,6 @@ export const EditorEvents = {
|
||||
goForward: "editor-events:go-forward",
|
||||
saveScroll: "editor-events:save-scroll",
|
||||
newNote: "editor-events:new-note",
|
||||
downloadCsv: "editor-events:download-csv"
|
||||
downloadCsv: "editor-events:download-csv",
|
||||
getLinkData: "editor-events:get-link-data"
|
||||
} as const;
|
||||
|
||||
@@ -88,12 +88,14 @@ import { strings } from "@notesnook/intl";
|
||||
import { InlineCode } from "./extensions/inline-code/inline-code.js";
|
||||
import { FontLigature } from "./extensions/font-ligature/font-ligature.js";
|
||||
import { SearchResult } from "./extensions/search-result/search-result.js";
|
||||
import { LinkData } from "./types.js";
|
||||
|
||||
interface TiptapStorage {
|
||||
dateFormat?: DateTimeOptions["dateFormat"];
|
||||
timeFormat?: DateTimeOptions["timeFormat"];
|
||||
dayFormat?: DateTimeOptions["dayFormat"];
|
||||
openLink?: (url: string, openInNewTab?: boolean) => void;
|
||||
getLinkData?: (url: string) => Promise<LinkData | undefined>;
|
||||
downloadAttachment?: (attachment: Attachment) => void;
|
||||
openAttachmentPicker?: (type: AttachmentType) => void;
|
||||
previewAttachment?: (attachment: Attachment) => void;
|
||||
@@ -148,6 +150,7 @@ const useTiptap = (
|
||||
openAttachmentPicker,
|
||||
previewAttachment,
|
||||
openLink,
|
||||
getLinkData,
|
||||
onBeforeCreate,
|
||||
dateFormat,
|
||||
timeFormat,
|
||||
@@ -285,7 +288,13 @@ const useTiptap = (
|
||||
}).configure({
|
||||
openOnClick: !isMobile,
|
||||
autolink: false,
|
||||
linkOnPaste: true
|
||||
linkOnPaste: true,
|
||||
protocols: [
|
||||
{
|
||||
scheme: "nn",
|
||||
optionalSlashes: true
|
||||
}
|
||||
]
|
||||
}),
|
||||
Table.configure({
|
||||
resizable: true,
|
||||
@@ -395,6 +404,7 @@ const useTiptap = (
|
||||
editor.storage.createInternalLink = createInternalLink;
|
||||
editor.storage.getAttachmentData = getAttachmentData;
|
||||
editor.storage.downloadCsvTable = downloadCsvTable;
|
||||
editor.storage.getLinkData = getLinkData;
|
||||
|
||||
if (onBeforeCreate) onBeforeCreate({ editor });
|
||||
},
|
||||
|
||||
@@ -19,20 +19,24 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { ToolProps } from "../types.js";
|
||||
import { ToolButton } from "../components/tool-button.js";
|
||||
import { useRef, useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { ResponsivePresenter } from "../../components/responsive/index.js";
|
||||
import { LinkPopup } from "../popups/link-popup.js";
|
||||
import { useToolbarLocation } from "../stores/toolbar-store.js";
|
||||
import { MoreTools } from "../components/more-tools.js";
|
||||
import { useRefValue } from "../../hooks/use-ref-value.js";
|
||||
import { findMark, selectionToOffset } from "../../utils/prosemirror.js";
|
||||
import { Flex, Link } from "@theme-ui/components";
|
||||
import { Flex, Link, Text } from "@theme-ui/components";
|
||||
import { ImageNode } from "../../extensions/image/index.js";
|
||||
import { Link as LinkNode } from "../../extensions/link/index.js";
|
||||
import { getMarkAttributes } from "@tiptap/core";
|
||||
import { useHoverPopupContext } from "../floating-menus/hover-popup/context.js";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { find } from "linkifyjs";
|
||||
import { Icons } from "../icons.js";
|
||||
import { mdiNoteOutline, mdiBookOutline, mdiPound } from "@mdi/js";
|
||||
import { Icon } from "@notesnook/ui";
|
||||
import { LinkData } from "../../types.js";
|
||||
|
||||
export function LinkSettings(props: ToolProps) {
|
||||
const { editor } = props;
|
||||
@@ -204,36 +208,62 @@ export function OpenLink(props: ToolProps) {
|
||||
);
|
||||
const { node } = selectedNode.current || {};
|
||||
const link = node ? findMark(node, "link") : null;
|
||||
if (!link) return null;
|
||||
const href = link?.attrs.href;
|
||||
if (!href) return null;
|
||||
const href = link?.attrs.href ?? null;
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [linkData, setLinkData] = useState<LinkData | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
if (!href) return;
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const result = await editor.storage.getLinkData?.(href);
|
||||
setLinkData(result);
|
||||
setLoading(false);
|
||||
} catch (e) {
|
||||
setLoading(false);
|
||||
}
|
||||
})();
|
||||
}, [href]);
|
||||
|
||||
if (!link || !href) return null;
|
||||
|
||||
const title = linkData?.title || href;
|
||||
|
||||
return (
|
||||
<Flex sx={{ alignItems: "center" }}>
|
||||
<Link
|
||||
href={href}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
editor.storage.openLink?.(href);
|
||||
hide();
|
||||
}}
|
||||
target="_blank"
|
||||
variant="body"
|
||||
sx={{
|
||||
fontSize: "subBody",
|
||||
fontFamily: "body",
|
||||
mr: 1,
|
||||
color: "accent",
|
||||
maxWidth: [150, 250],
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
":visited": { color: "accent" },
|
||||
":hover": { color: "accent", opacity: 0.8 }
|
||||
}}
|
||||
>
|
||||
{href}
|
||||
</Link>
|
||||
{linkData?.type && (
|
||||
<LinkTypeIcon type={linkData.type} metadata={linkData.metadata} />
|
||||
)}
|
||||
{loading ? (
|
||||
<Text sx={{ fontSize: "subBody" }}>{strings.loading()}</Text>
|
||||
) : (
|
||||
<Link
|
||||
href={href}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
editor.storage.openLink?.(href);
|
||||
hide();
|
||||
}}
|
||||
target="_blank"
|
||||
variant="body"
|
||||
sx={{
|
||||
fontSize: "subBody",
|
||||
fontFamily: "body",
|
||||
mr: 4,
|
||||
color: "accent",
|
||||
maxWidth: [150, 250],
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
":visited": { color: "accent" },
|
||||
":hover": { color: "accent", opacity: 0.8 }
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Link>
|
||||
)}
|
||||
<ToolButton
|
||||
icon={props.icon}
|
||||
title={props.title}
|
||||
@@ -335,6 +365,31 @@ function LinkTool(props: LinkToolProps) {
|
||||
);
|
||||
}
|
||||
|
||||
const LINK_TYPE_ICONS: Record<LinkData["type"], string> = {
|
||||
note: mdiNoteOutline,
|
||||
notebook: mdiBookOutline,
|
||||
tag: mdiPound,
|
||||
color: Icons.circle
|
||||
};
|
||||
|
||||
function LinkTypeIcon({
|
||||
type,
|
||||
metadata
|
||||
}: {
|
||||
type: LinkData["type"];
|
||||
metadata?: LinkData["metadata"];
|
||||
}) {
|
||||
const path = LINK_TYPE_ICONS[type];
|
||||
if (!path) return null;
|
||||
|
||||
const color =
|
||||
type === "color" && metadata?.colorCode
|
||||
? metadata.colorCode
|
||||
: "icon-secondary";
|
||||
|
||||
return <Icon path={path} color={color} size={13} sx={{ mr: 1 }} />;
|
||||
}
|
||||
|
||||
export function isInternalLink(href?: string | null) {
|
||||
return typeof href === "string" ? href.startsWith("nn://") : false;
|
||||
}
|
||||
|
||||
@@ -25,6 +25,12 @@ export type PermissionRequestEvent = CustomEvent<{
|
||||
silent: boolean;
|
||||
}>;
|
||||
|
||||
export type LinkData = {
|
||||
type: "note" | "notebook" | "color" | "tag";
|
||||
title?: string;
|
||||
metadata?: Record<string, string | undefined>;
|
||||
};
|
||||
|
||||
export class Editor extends TiptapEditor {
|
||||
private mutex: Mutex = new Mutex();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user