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 no-case-declarations */
|
||||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
import { isFeatureAvailable, useAreFeaturesAvailable } from "@notesnook/common";
|
import { isFeatureAvailable, useAreFeaturesAvailable } from "@notesnook/common";
|
||||||
import { ItemReference } from "@notesnook/core";
|
import { ItemReference, parseInternalLink } from "@notesnook/core";
|
||||||
import type { Attachment } from "@notesnook/editor";
|
import type { Attachment } from "@notesnook/editor";
|
||||||
import { EditorEvents } from "@notesnook/editor-mobile/src/utils/editor-events";
|
import { EditorEvents } from "@notesnook/editor-mobile/src/utils/editor-events";
|
||||||
import { NativeEvents } from "@notesnook/editor-mobile/src/utils/native-events";
|
import { NativeEvents } from "@notesnook/editor-mobile/src/utils/native-events";
|
||||||
@@ -471,9 +471,8 @@ export const useEditorEvents = (
|
|||||||
relationType: "from",
|
relationType: "from",
|
||||||
title: strings.dataTypesPluralCamelCase.reminder(),
|
title: strings.dataTypesPluralCamelCase.reminder(),
|
||||||
onAdd: async () => {
|
onAdd: async () => {
|
||||||
const reminderFeature = await isFeatureAvailable(
|
const reminderFeature =
|
||||||
"activeReminders"
|
await isFeatureAvailable("activeReminders");
|
||||||
);
|
|
||||||
if (!reminderFeature.isAllowed) {
|
if (!reminderFeature.isAllowed) {
|
||||||
ToastManager.show({
|
ToastManager.show({
|
||||||
type: "info",
|
type: "info",
|
||||||
@@ -533,7 +532,59 @@ export const useEditorEvents = (
|
|||||||
downloadAttachment((editorMessage.value as Attachment)?.hash, true);
|
downloadAttachment((editorMessage.value as Attachment)?.hash, true);
|
||||||
break;
|
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: {
|
case EditorEvents.getAttachmentData: {
|
||||||
const data = (editorMessage.value as any)?.attachment as Attachment;
|
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",
|
"name": "@notesnook/mobile",
|
||||||
"version": "3.3.17",
|
"version": "3.3.18",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@notesnook/mobile",
|
"name": "@notesnook/mobile",
|
||||||
"version": "3.3.17",
|
"version": "3.3.18",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "GPL-3.0-or-later",
|
"license": "GPL-3.0-or-later",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ import { UpgradeDialog } from "../../dialogs/buy-dialog/upgrade-dialog";
|
|||||||
import { ConfirmDialog } from "../../dialogs/confirm";
|
import { ConfirmDialog } from "../../dialogs/confirm";
|
||||||
import { strings } from "@notesnook/intl";
|
import { strings } from "@notesnook/intl";
|
||||||
import { handleInternalLink } from "../../common";
|
import { handleInternalLink } from "../../common";
|
||||||
|
import { db } from "../../common/db";
|
||||||
|
|
||||||
export type OnChangeHandler = (
|
export type OnChangeHandler = (
|
||||||
content: () => string,
|
content: () => string,
|
||||||
@@ -426,6 +427,48 @@ function TipTap(props: TipTapProps) {
|
|||||||
const link = parseInternalLink(url);
|
const link = parseInternalLink(url);
|
||||||
if (link) handleInternalLink(url, openInNewTab);
|
if (link) handleInternalLink(url, openInNewTab);
|
||||||
else window.open(url, "_blank");
|
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/intl": "file:../intl",
|
||||||
"@notesnook/theme": "file:../theme",
|
"@notesnook/theme": "file:../theme",
|
||||||
"@notesnook/ui": "file:../ui",
|
"@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/core": "2.6.6",
|
||||||
"@tiptap/extension-blockquote": "^2.6.6",
|
"@tiptap/extension-blockquote": "^2.6.6",
|
||||||
"@tiptap/extension-bullet-list": "^2.6.6",
|
"@tiptap/extension-bullet-list": "^2.6.6",
|
||||||
|
|||||||
@@ -162,6 +162,11 @@ const Tiptap = ({
|
|||||||
attachment
|
attachment
|
||||||
) as Promise<string | undefined>;
|
) as Promise<string | undefined>;
|
||||||
},
|
},
|
||||||
|
getLinkData: (url: string) => {
|
||||||
|
return postAsyncWithTimeout(EditorEvents.getLinkData, {
|
||||||
|
url: url
|
||||||
|
});
|
||||||
|
},
|
||||||
createInternalLink(attributes) {
|
createInternalLink(attributes) {
|
||||||
return postAsyncWithTimeout(EditorEvents.createInternalLink, {
|
return postAsyncWithTimeout(EditorEvents.createInternalLink, {
|
||||||
attributes
|
attributes
|
||||||
|
|||||||
@@ -55,5 +55,6 @@ export const EditorEvents = {
|
|||||||
goForward: "editor-events:go-forward",
|
goForward: "editor-events:go-forward",
|
||||||
saveScroll: "editor-events:save-scroll",
|
saveScroll: "editor-events:save-scroll",
|
||||||
newNote: "editor-events:new-note",
|
newNote: "editor-events:new-note",
|
||||||
downloadCsv: "editor-events:download-csv"
|
downloadCsv: "editor-events:download-csv",
|
||||||
|
getLinkData: "editor-events:get-link-data"
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@@ -88,12 +88,14 @@ import { strings } from "@notesnook/intl";
|
|||||||
import { InlineCode } from "./extensions/inline-code/inline-code.js";
|
import { InlineCode } from "./extensions/inline-code/inline-code.js";
|
||||||
import { FontLigature } from "./extensions/font-ligature/font-ligature.js";
|
import { FontLigature } from "./extensions/font-ligature/font-ligature.js";
|
||||||
import { SearchResult } from "./extensions/search-result/search-result.js";
|
import { SearchResult } from "./extensions/search-result/search-result.js";
|
||||||
|
import { LinkData } from "./types.js";
|
||||||
|
|
||||||
interface TiptapStorage {
|
interface TiptapStorage {
|
||||||
dateFormat?: DateTimeOptions["dateFormat"];
|
dateFormat?: DateTimeOptions["dateFormat"];
|
||||||
timeFormat?: DateTimeOptions["timeFormat"];
|
timeFormat?: DateTimeOptions["timeFormat"];
|
||||||
dayFormat?: DateTimeOptions["dayFormat"];
|
dayFormat?: DateTimeOptions["dayFormat"];
|
||||||
openLink?: (url: string, openInNewTab?: boolean) => void;
|
openLink?: (url: string, openInNewTab?: boolean) => void;
|
||||||
|
getLinkData?: (url: string) => Promise<LinkData | undefined>;
|
||||||
downloadAttachment?: (attachment: Attachment) => void;
|
downloadAttachment?: (attachment: Attachment) => void;
|
||||||
openAttachmentPicker?: (type: AttachmentType) => void;
|
openAttachmentPicker?: (type: AttachmentType) => void;
|
||||||
previewAttachment?: (attachment: Attachment) => void;
|
previewAttachment?: (attachment: Attachment) => void;
|
||||||
@@ -148,6 +150,7 @@ const useTiptap = (
|
|||||||
openAttachmentPicker,
|
openAttachmentPicker,
|
||||||
previewAttachment,
|
previewAttachment,
|
||||||
openLink,
|
openLink,
|
||||||
|
getLinkData,
|
||||||
onBeforeCreate,
|
onBeforeCreate,
|
||||||
dateFormat,
|
dateFormat,
|
||||||
timeFormat,
|
timeFormat,
|
||||||
@@ -285,7 +288,13 @@ const useTiptap = (
|
|||||||
}).configure({
|
}).configure({
|
||||||
openOnClick: !isMobile,
|
openOnClick: !isMobile,
|
||||||
autolink: false,
|
autolink: false,
|
||||||
linkOnPaste: true
|
linkOnPaste: true,
|
||||||
|
protocols: [
|
||||||
|
{
|
||||||
|
scheme: "nn",
|
||||||
|
optionalSlashes: true
|
||||||
|
}
|
||||||
|
]
|
||||||
}),
|
}),
|
||||||
Table.configure({
|
Table.configure({
|
||||||
resizable: true,
|
resizable: true,
|
||||||
@@ -395,6 +404,7 @@ const useTiptap = (
|
|||||||
editor.storage.createInternalLink = createInternalLink;
|
editor.storage.createInternalLink = createInternalLink;
|
||||||
editor.storage.getAttachmentData = getAttachmentData;
|
editor.storage.getAttachmentData = getAttachmentData;
|
||||||
editor.storage.downloadCsvTable = downloadCsvTable;
|
editor.storage.downloadCsvTable = downloadCsvTable;
|
||||||
|
editor.storage.getLinkData = getLinkData;
|
||||||
|
|
||||||
if (onBeforeCreate) onBeforeCreate({ editor });
|
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 { ToolProps } from "../types.js";
|
||||||
import { ToolButton } from "../components/tool-button.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 { ResponsivePresenter } from "../../components/responsive/index.js";
|
||||||
import { LinkPopup } from "../popups/link-popup.js";
|
import { LinkPopup } from "../popups/link-popup.js";
|
||||||
import { useToolbarLocation } from "../stores/toolbar-store.js";
|
import { useToolbarLocation } from "../stores/toolbar-store.js";
|
||||||
import { MoreTools } from "../components/more-tools.js";
|
import { MoreTools } from "../components/more-tools.js";
|
||||||
import { useRefValue } from "../../hooks/use-ref-value.js";
|
import { useRefValue } from "../../hooks/use-ref-value.js";
|
||||||
import { findMark, selectionToOffset } from "../../utils/prosemirror.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 { ImageNode } from "../../extensions/image/index.js";
|
||||||
import { Link as LinkNode } from "../../extensions/link/index.js";
|
import { Link as LinkNode } from "../../extensions/link/index.js";
|
||||||
import { getMarkAttributes } from "@tiptap/core";
|
import { getMarkAttributes } from "@tiptap/core";
|
||||||
import { useHoverPopupContext } from "../floating-menus/hover-popup/context.js";
|
import { useHoverPopupContext } from "../floating-menus/hover-popup/context.js";
|
||||||
import { strings } from "@notesnook/intl";
|
import { strings } from "@notesnook/intl";
|
||||||
import { find } from "linkifyjs";
|
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) {
|
export function LinkSettings(props: ToolProps) {
|
||||||
const { editor } = props;
|
const { editor } = props;
|
||||||
@@ -204,36 +208,62 @@ export function OpenLink(props: ToolProps) {
|
|||||||
);
|
);
|
||||||
const { node } = selectedNode.current || {};
|
const { node } = selectedNode.current || {};
|
||||||
const link = node ? findMark(node, "link") : null;
|
const link = node ? findMark(node, "link") : null;
|
||||||
if (!link) return null;
|
const href = link?.attrs.href ?? null;
|
||||||
const href = link?.attrs.href;
|
const [loading, setLoading] = useState(false);
|
||||||
if (!href) return null;
|
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 (
|
return (
|
||||||
<Flex sx={{ alignItems: "center" }}>
|
<Flex sx={{ alignItems: "center" }}>
|
||||||
<Link
|
{linkData?.type && (
|
||||||
href={href}
|
<LinkTypeIcon type={linkData.type} metadata={linkData.metadata} />
|
||||||
onClick={(e) => {
|
)}
|
||||||
e.preventDefault();
|
{loading ? (
|
||||||
editor.storage.openLink?.(href);
|
<Text sx={{ fontSize: "subBody" }}>{strings.loading()}</Text>
|
||||||
hide();
|
) : (
|
||||||
}}
|
<Link
|
||||||
target="_blank"
|
href={href}
|
||||||
variant="body"
|
onClick={(e) => {
|
||||||
sx={{
|
e.preventDefault();
|
||||||
fontSize: "subBody",
|
editor.storage.openLink?.(href);
|
||||||
fontFamily: "body",
|
hide();
|
||||||
mr: 1,
|
}}
|
||||||
color: "accent",
|
target="_blank"
|
||||||
maxWidth: [150, 250],
|
variant="body"
|
||||||
overflow: "hidden",
|
sx={{
|
||||||
textOverflow: "ellipsis",
|
fontSize: "subBody",
|
||||||
whiteSpace: "nowrap",
|
fontFamily: "body",
|
||||||
":visited": { color: "accent" },
|
mr: 4,
|
||||||
":hover": { color: "accent", opacity: 0.8 }
|
color: "accent",
|
||||||
}}
|
maxWidth: [150, 250],
|
||||||
>
|
overflow: "hidden",
|
||||||
{href}
|
textOverflow: "ellipsis",
|
||||||
</Link>
|
whiteSpace: "nowrap",
|
||||||
|
":visited": { color: "accent" },
|
||||||
|
":hover": { color: "accent", opacity: 0.8 }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
<ToolButton
|
<ToolButton
|
||||||
icon={props.icon}
|
icon={props.icon}
|
||||||
title={props.title}
|
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) {
|
export function isInternalLink(href?: string | null) {
|
||||||
return typeof href === "string" ? href.startsWith("nn://") : false;
|
return typeof href === "string" ? href.startsWith("nn://") : false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,12 @@ export type PermissionRequestEvent = CustomEvent<{
|
|||||||
silent: boolean;
|
silent: boolean;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
export type LinkData = {
|
||||||
|
type: "note" | "notebook" | "color" | "tag";
|
||||||
|
title?: string;
|
||||||
|
metadata?: Record<string, string | undefined>;
|
||||||
|
};
|
||||||
|
|
||||||
export class Editor extends TiptapEditor {
|
export class Editor extends TiptapEditor {
|
||||||
private mutex: Mutex = new Mutex();
|
private mutex: Mutex = new Mutex();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user