mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-02-24 12:12:54 +01:00
web: add full support for adding internal links
This commit is contained in:
@@ -37,6 +37,7 @@ import { ThemeMetadata } from "@notesnook/themes-server";
|
||||
import { Color, Reminder, Tag } from "@notesnook/core";
|
||||
import { AuthenticatorType } from "@notesnook/core/dist/api/user-manager";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { LinkAttributes } from "@notesnook/editor/dist/extensions/link";
|
||||
|
||||
type DialogTypes = typeof Dialogs;
|
||||
type DialogIds = keyof DialogTypes;
|
||||
@@ -99,6 +100,19 @@ export function showAddNotebookDialog(parentId?: string) {
|
||||
));
|
||||
}
|
||||
|
||||
export function showNoteLinkingDialog(attr?: LinkAttributes) {
|
||||
return showDialog<"NoteLinkingDialog", LinkAttributes | undefined>(
|
||||
"NoteLinkingDialog",
|
||||
(Dialog, perform) => (
|
||||
<Dialog
|
||||
attributes={attr}
|
||||
onDone={(link) => perform(link)}
|
||||
onClose={() => perform(undefined)}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export async function showEditNotebookDialog(notebookId: string) {
|
||||
const notebook = await db.notebooks.notebook(notebookId);
|
||||
if (!notebook) return;
|
||||
|
||||
@@ -59,6 +59,8 @@ import { EditorActionBar } from "./action-bar";
|
||||
import { UnlockView } from "../unlock";
|
||||
import DiffViewer from "../diff-viewer";
|
||||
import TableOfContents from "./table-of-contents";
|
||||
import { showNoteLinkingDialog } from "../../common/dialog-controller";
|
||||
import { scrollIntoViewById } from "@notesnook/editor";
|
||||
|
||||
const PDFPreview = React.lazy(() => import("../pdf-preview"));
|
||||
|
||||
@@ -428,6 +430,7 @@ export function Editor(props: EditorProps) {
|
||||
isMobile: false
|
||||
};
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
useScrollToBlock(id);
|
||||
|
||||
useEffect(() => {
|
||||
const event = AppEventManager.subscribe(
|
||||
@@ -528,13 +531,18 @@ export function Editor(props: EditorProps) {
|
||||
const mime = type === "file" ? "*/*" : "image/*";
|
||||
insertAttachment(mime).then((file) => {
|
||||
if (!file) return;
|
||||
// editor.current?.attachFile(file);
|
||||
// editor.attachFile(file);
|
||||
});
|
||||
}}
|
||||
onAttachFile={async (file) => {
|
||||
const result = await attachFile(file);
|
||||
if (!result) return;
|
||||
// editor.current?.attachFile(result);
|
||||
// editor.attachFile(result);
|
||||
}}
|
||||
onInsertInternalLink={async (attributes) => {
|
||||
const link = await showNoteLinkingDialog(attributes);
|
||||
console.log(link);
|
||||
return link;
|
||||
}}
|
||||
>
|
||||
{headless ? null : (
|
||||
@@ -806,6 +814,16 @@ function useDragOverlay() {
|
||||
return [dropElementRef, overlayRef] as const;
|
||||
}
|
||||
|
||||
function useScrollToBlock(id: string) {
|
||||
const blockId = useEditorStore(
|
||||
(store) => store.getSession(id)?.activeBlockId
|
||||
);
|
||||
useEffect(() => {
|
||||
if (!blockId) return;
|
||||
scrollIntoViewById(blockId);
|
||||
}, [blockId]);
|
||||
}
|
||||
|
||||
function isFile(e: DragEvent) {
|
||||
return (
|
||||
e.dataTransfer &&
|
||||
@@ -815,6 +833,9 @@ function isFile(e: DragEvent) {
|
||||
}
|
||||
|
||||
function restoreScrollPosition(id: string) {
|
||||
const session = useEditorStore.getState().getActiveSession();
|
||||
if (session?.activeBlockId) return scrollIntoViewById(session.activeBlockId);
|
||||
|
||||
const scrollContainer = document.getElementById(`${id}_editorScroll`);
|
||||
const scrollPosition = Config.get(`${id}:scroll-position`, 0);
|
||||
if (scrollContainer) {
|
||||
|
||||
@@ -58,10 +58,13 @@ import { showBuyDialog } from "../../common/dialog-controller";
|
||||
import { useStore as useSettingsStore } from "../../stores/setting-store";
|
||||
import { debounce } from "@notesnook/common";
|
||||
import { ScopedThemeProvider } from "../theme-provider";
|
||||
import { writeText } from "clipboard-polyfill";
|
||||
import { useStore as useThemeStore } from "../../stores/theme-store";
|
||||
import { toBlobURL } from "@notesnook/editor/dist/utils/downloader";
|
||||
import { getChangedNodes } from "@notesnook/editor/dist/utils/prosemirror";
|
||||
import { LinkAttributes } from "@notesnook/editor/dist/extensions/link";
|
||||
import { writeToClipboard } from "../../utils/clipboard";
|
||||
import { useEditorStore } from "../../stores/editor-store";
|
||||
import { parseInternalLink } from "@notesnook/core";
|
||||
|
||||
export type OnChangeHandler = (content: () => string) => void;
|
||||
type TipTapProps = {
|
||||
@@ -74,6 +77,9 @@ type TipTapProps = {
|
||||
onInsertAttachment?: (type: AttachmentType) => void;
|
||||
onDownloadAttachment?: (attachment: Attachment) => void;
|
||||
onPreviewAttachment?: (attachment: Attachment) => void;
|
||||
onInsertInternalLink?: (
|
||||
attributes?: LinkAttributes
|
||||
) => Promise<LinkAttributes | undefined>;
|
||||
onAttachFile?: (file: File) => void;
|
||||
onFocus?: () => void;
|
||||
content?: () => string | undefined;
|
||||
@@ -108,6 +114,7 @@ function TipTap(props: TipTapProps) {
|
||||
onInsertAttachment,
|
||||
onDownloadAttachment,
|
||||
onPreviewAttachment,
|
||||
onInsertInternalLink,
|
||||
onAttachFile,
|
||||
onContentChange,
|
||||
onFocus = () => {},
|
||||
@@ -217,9 +224,14 @@ function TipTap(props: TipTapProps) {
|
||||
const preventSave = transaction?.getMeta("preventSave") as boolean;
|
||||
if (preventSave || !editor.isEditable || !onChange) return;
|
||||
|
||||
onChange(() =>
|
||||
getHTMLFromFragment(editor.state.doc.content, editor.schema)
|
||||
);
|
||||
onChange(() => {
|
||||
const html = getHTMLFromFragment(
|
||||
editor.state.doc.content,
|
||||
editor.schema
|
||||
);
|
||||
console.log(html);
|
||||
return html;
|
||||
});
|
||||
},
|
||||
onDestroy: () => {
|
||||
useEditorManager.getState().setEditor(id);
|
||||
@@ -230,8 +242,8 @@ function TipTap(props: TipTapProps) {
|
||||
canUndo: editor.can().undo()
|
||||
});
|
||||
},
|
||||
copyToClipboard(text) {
|
||||
writeText(text);
|
||||
copyToClipboard(text, html) {
|
||||
writeToClipboard({ "text/plain": text, "text/html": html });
|
||||
},
|
||||
onSelectionUpdate: debounce(({ editor, transaction }) => {
|
||||
const isEmptySelection = transaction.selection.empty;
|
||||
@@ -260,21 +272,17 @@ function TipTap(props: TipTapProps) {
|
||||
};
|
||||
});
|
||||
}, 500),
|
||||
onOpenAttachmentPicker: (_editor, type) => {
|
||||
onInsertAttachment?.(type);
|
||||
return true;
|
||||
},
|
||||
onDownloadAttachment: (_editor, attachment) => {
|
||||
onDownloadAttachment?.(attachment);
|
||||
return true;
|
||||
},
|
||||
onPreviewAttachment(_editor, attachment) {
|
||||
onPreviewAttachment?.(attachment);
|
||||
return true;
|
||||
},
|
||||
onOpenLink: (url) => {
|
||||
window.open(url, "_blank");
|
||||
return true;
|
||||
openAttachmentPicker: onInsertAttachment,
|
||||
downloadAttachment: onDownloadAttachment,
|
||||
previewAttachment: onPreviewAttachment,
|
||||
createInternalLink: onInsertInternalLink,
|
||||
openLink: (url) => {
|
||||
const link = parseInternalLink(url);
|
||||
if (link && link.type === "note") {
|
||||
useEditorStore.getState().openSession(link.id, {
|
||||
activeBlockId: link.params?.blockId || undefined
|
||||
});
|
||||
} else window.open(url, "_blank");
|
||||
}
|
||||
};
|
||||
}, [readonly, nonce, doubleSpacedLines, dateFormat, timeFormat]);
|
||||
@@ -412,25 +420,25 @@ function toIEditor(editor: Editor): IEditor {
|
||||
return {
|
||||
focus: ({ position, scrollIntoView } = {}) => {
|
||||
if (typeof position === "object")
|
||||
editor.current?.chain().focus().setTextSelection(position).run();
|
||||
editor.chain().focus().setTextSelection(position).run();
|
||||
else
|
||||
editor.current?.commands.focus(position, {
|
||||
editor.commands.focus(position, {
|
||||
scrollIntoView
|
||||
});
|
||||
},
|
||||
undo: () => editor.current?.commands.undo(),
|
||||
redo: () => editor.current?.commands.redo(),
|
||||
undo: () => editor.commands.undo(),
|
||||
redo: () => editor.commands.redo(),
|
||||
getMediaHashes: () => {
|
||||
if (!editor.current) return [];
|
||||
if (!editor) return [];
|
||||
const hashes: string[] = [];
|
||||
editor.current.state.doc.descendants((n) => {
|
||||
editor.state.doc.descendants((n) => {
|
||||
if (typeof n.attrs.hash === "string") hashes.push(n.attrs.hash);
|
||||
});
|
||||
return hashes;
|
||||
},
|
||||
updateContent: (content) => {
|
||||
const { from, to } = editor.state.selection;
|
||||
editor.current
|
||||
editor
|
||||
?.chain()
|
||||
.command(({ tr }) => {
|
||||
tr.setMeta("preventSave", true);
|
||||
@@ -445,21 +453,21 @@ function toIEditor(editor: Editor): IEditor {
|
||||
},
|
||||
attachFile: (file: Attachment) => {
|
||||
if (file.dataurl) {
|
||||
editor.current?.commands.insertImage({
|
||||
editor.commands.insertImage({
|
||||
...file,
|
||||
bloburl: toBlobURL(file.dataurl, file.hash)
|
||||
});
|
||||
} else editor.current?.commands.insertAttachment(file);
|
||||
} else editor.commands.insertAttachment(file);
|
||||
},
|
||||
loadWebClip: (hash, src) =>
|
||||
editor.current?.commands.updateWebClip({ hash }, { src }),
|
||||
editor.commands.updateWebClip({ hash }, { src }),
|
||||
loadImage: (hash, dataurl) =>
|
||||
editor.current?.commands.updateImage(
|
||||
editor.commands.updateImage(
|
||||
{ hash },
|
||||
{ hash, bloburl: toBlobURL(dataurl, hash), preventUpdate: true }
|
||||
),
|
||||
sendAttachmentProgress: (hash, type, progress) =>
|
||||
editor.current?.commands.setAttachmentProgress({
|
||||
editor.commands.setAttachmentProgress({
|
||||
hash,
|
||||
type,
|
||||
progress
|
||||
|
||||
@@ -209,7 +209,8 @@ import {
|
||||
mdiDesktopClassic,
|
||||
mdiBellBadgeOutline,
|
||||
mdiDotsHorizontal,
|
||||
mdiFormatListBulleted
|
||||
mdiFormatListBulleted,
|
||||
mdiLink
|
||||
} from "@mdi/js";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { Theme } from "@notesnook/theme";
|
||||
@@ -405,6 +406,7 @@ export const Copy = createIcon(mdiContentCopy);
|
||||
export const Refresh = createIcon(mdiRefresh);
|
||||
export const Clock = createIcon(mdiClockTimeFiveOutline);
|
||||
export const Duplicate = createIcon(mdiContentDuplicate);
|
||||
export const InternalLink = createIcon(mdiLink);
|
||||
export const Select = createIcon(mdiCheckboxMultipleMarkedCircleOutline);
|
||||
export const NotebookEdit = createIcon(mdiBookEditOutline);
|
||||
export const DeleteForver = createIcon(mdiDeleteForeverOutline);
|
||||
|
||||
@@ -42,6 +42,7 @@ import {
|
||||
Publish,
|
||||
Export,
|
||||
Duplicate,
|
||||
InternalLink,
|
||||
Sync,
|
||||
Trash,
|
||||
Circle,
|
||||
@@ -59,7 +60,7 @@ import {
|
||||
showAddTagsDialog,
|
||||
showMoveNoteDialog
|
||||
} from "../../common/dialog-controller";
|
||||
import { store, useStore } from "../../stores/note-store";
|
||||
import { store } from "../../stores/note-store";
|
||||
import { store as userstore } from "../../stores/user-store";
|
||||
import { useEditorStore } from "../../stores/editor-store";
|
||||
import { store as tagStore } from "../../stores/tag-store";
|
||||
@@ -83,7 +84,8 @@ import {
|
||||
Note,
|
||||
Notebook as NotebookItem,
|
||||
Tag,
|
||||
DefaultColors
|
||||
DefaultColors,
|
||||
createInternalLink
|
||||
} from "@notesnook/core";
|
||||
import { MenuItem } from "@notesnook/ui";
|
||||
import {
|
||||
@@ -93,6 +95,7 @@ import {
|
||||
} from "../list-container/types";
|
||||
import { SchemeColors } from "@notesnook/theme";
|
||||
import Vault from "../../common/vault";
|
||||
import { writeToClipboard } from "../../utils/clipboard";
|
||||
|
||||
type NoteProps = {
|
||||
tags?: TagsWithDateEdited;
|
||||
@@ -499,6 +502,20 @@ const menuItems: (
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
key: "copy-link",
|
||||
title: "Copy internal link",
|
||||
icon: InternalLink.path,
|
||||
onClick: () => {
|
||||
const link = createInternalLink("note", note.id);
|
||||
writeToClipboard({
|
||||
"text/plain": link,
|
||||
"text/html": `<a href="${link}">${note.title}</a>`,
|
||||
"text/markdown": `[${note.title}](${link})`
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
key: "duplicate",
|
||||
|
||||
@@ -52,6 +52,7 @@ const MigrationDialog = React.lazy(() => import("./migration-dialog"));
|
||||
const EmailChangeDialog = React.lazy(() => import("./email-change-dialog"));
|
||||
const AddTagsDialog = React.lazy(() => import("./add-tags-dialog"));
|
||||
const ThemeDetailsDialog = React.lazy(() => import("./theme-details-dialog"));
|
||||
const NoteLinkingDialog = React.lazy(() => import("./note-linking-dialog"));
|
||||
|
||||
export const Dialogs = {
|
||||
AddNotebookDialog,
|
||||
@@ -79,5 +80,6 @@ export const Dialogs = {
|
||||
EmailChangeDialog,
|
||||
AddTagsDialog,
|
||||
SettingsDialog,
|
||||
ThemeDetailsDialog
|
||||
ThemeDetailsDialog,
|
||||
NoteLinkingDialog
|
||||
};
|
||||
|
||||
181
apps/web/src/dialogs/note-linking-dialog.tsx
Normal file
181
apps/web/src/dialogs/note-linking-dialog.tsx
Normal file
@@ -0,0 +1,181 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Perform } from "../common/dialog-controller";
|
||||
import Field from "../components/field";
|
||||
import Dialog from "../components/dialog";
|
||||
import { useState } from "react";
|
||||
import { db } from "../common/db";
|
||||
import {
|
||||
ContentBlock,
|
||||
Note as NoteType,
|
||||
VirtualizedGrouping,
|
||||
createInternalLink
|
||||
} from "@notesnook/core";
|
||||
import { VirtualizedList } from "../components/virtualized-list";
|
||||
import { ResolvedItem } from "../components/list-container/resolved-item";
|
||||
import { Button, Flex, Text } from "@theme-ui/components";
|
||||
import { ScrollContainer } from "@notesnook/ui";
|
||||
import { LinkAttributes } from "@notesnook/editor/dist/extensions/link";
|
||||
|
||||
export type NoteLinkingDialogProps = {
|
||||
attributes?: LinkAttributes;
|
||||
onClose: Perform;
|
||||
onDone: Perform<LinkAttributes>;
|
||||
};
|
||||
|
||||
export default function NoteLinkingDialog(props: NoteLinkingDialogProps) {
|
||||
const { attributes } = props;
|
||||
const [notes, setNotes] = useState<VirtualizedGrouping<NoteType>>();
|
||||
const [selectedNote, setSelectedNote] = useState<NoteType>();
|
||||
const [blocks, setBlocks] = useState<ContentBlock[]>([]);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
isOpen={true}
|
||||
title={attributes ? "Edit internal link" : "Link to note"}
|
||||
width={500}
|
||||
onClose={() => props.onClose(false)}
|
||||
onOpen={async () => {
|
||||
setNotes(
|
||||
await db.notes.all.sorted(db.settings.getGroupOptions("home"))
|
||||
);
|
||||
}}
|
||||
positiveButton={{
|
||||
text: "Save",
|
||||
disabled: !selectedNote,
|
||||
onClick: () =>
|
||||
selectedNote
|
||||
? props.onDone({
|
||||
title: selectedNote.title,
|
||||
href: createInternalLink("note", selectedNote.id)
|
||||
})
|
||||
: null
|
||||
}}
|
||||
negativeButton={{ text: "Cancel", onClick: () => props.onClose(false) }}
|
||||
noScroll
|
||||
>
|
||||
<Flex variant="columnFill" sx={{ mx: 3, overflow: "hidden" }}>
|
||||
{selectedNote ? (
|
||||
<>
|
||||
<Field
|
||||
autoFocus
|
||||
placeholder="Type # to only search headings"
|
||||
sx={{ mx: 0 }}
|
||||
onChange={async (e) =>
|
||||
setNotes(await db.lookup.notes(e.target.value).sorted())
|
||||
}
|
||||
/>
|
||||
<Button variant="accentSecondary" sx={{ mt: 1, textAlign: "left" }}>
|
||||
Selected note: {selectedNote.title}
|
||||
</Button>
|
||||
<ScrollContainer>
|
||||
<VirtualizedList
|
||||
items={blocks}
|
||||
estimatedSize={34}
|
||||
mode="dynamic"
|
||||
itemGap={5}
|
||||
getItemKey={(i) => blocks[i].id}
|
||||
mt={1}
|
||||
renderItem={({ item }) => (
|
||||
<Button
|
||||
variant="menuitem"
|
||||
sx={{
|
||||
p: 1,
|
||||
width: "100%",
|
||||
textAlign: "left",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center"
|
||||
}}
|
||||
onClick={() => {
|
||||
props.onDone({
|
||||
title: selectedNote.title,
|
||||
href: createInternalLink("note", selectedNote.id, {
|
||||
blockId: item.id
|
||||
})
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
variant="body"
|
||||
sx={{ fontFamily: "monospace", whiteSpace: "pre-wrap" }}
|
||||
>
|
||||
{item.content}
|
||||
</Text>
|
||||
<Text
|
||||
variant="subBody"
|
||||
sx={{
|
||||
bg: "background-secondary",
|
||||
p: "small",
|
||||
px: 1,
|
||||
borderRadius: "default",
|
||||
alignSelf: "flex-start"
|
||||
}}
|
||||
>
|
||||
{item.type.toUpperCase()}
|
||||
</Text>
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
</ScrollContainer>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Field
|
||||
autoFocus
|
||||
placeholder="Search for a note to link to..."
|
||||
sx={{ mx: 0 }}
|
||||
onChange={async (e) =>
|
||||
setNotes(await db.lookup.notes(e.target.value).sorted())
|
||||
}
|
||||
/>
|
||||
{notes && (
|
||||
<ScrollContainer>
|
||||
<VirtualizedList
|
||||
items={notes.placeholders}
|
||||
estimatedSize={28}
|
||||
itemGap={5}
|
||||
getItemKey={notes.key}
|
||||
mt={1}
|
||||
renderItem={({ index }) => (
|
||||
<ResolvedItem items={notes} index={index} type="note">
|
||||
{({ item: note }) => (
|
||||
<Button
|
||||
variant="menuitem"
|
||||
sx={{ p: 1, width: "100%", textAlign: "left" }}
|
||||
onClick={async () => {
|
||||
setSelectedNote(note);
|
||||
setBlocks(await db.notes.getBlocks(note.id));
|
||||
}}
|
||||
>
|
||||
<Text variant="body">{note.title}</Text>
|
||||
</Button>
|
||||
)}
|
||||
</ResolvedItem>
|
||||
)}
|
||||
/>
|
||||
</ScrollContainer>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -58,6 +58,11 @@ export type BaseEditorSession = {
|
||||
pinned?: boolean;
|
||||
preview?: boolean;
|
||||
title?: string;
|
||||
|
||||
/**
|
||||
* The id of block to scroll to after opening the session successfully.
|
||||
*/
|
||||
activeBlockId?: string;
|
||||
};
|
||||
|
||||
export type LockedEditorSession = BaseEditorSession & {
|
||||
@@ -196,7 +201,6 @@ class EditorStore extends BaseStore<EditorStore> {
|
||||
const session = getSession(activeSessionId);
|
||||
if (!session) return;
|
||||
|
||||
console.log("OPENING", session);
|
||||
if (session.type === "diff") openDiffSession(session.note.id, session.id);
|
||||
else if (session.type === "new") activateSession(session.id);
|
||||
else openSession(activeSessionId);
|
||||
@@ -240,7 +244,7 @@ class EditorStore extends BaseStore<EditorStore> {
|
||||
});
|
||||
};
|
||||
|
||||
activateSession = (id?: string) => {
|
||||
activateSession = (id?: string, activeBlockId?: string) => {
|
||||
const session = this.get().sessions.find((s) => s.id === id);
|
||||
if (!session) id = undefined;
|
||||
|
||||
@@ -262,6 +266,11 @@ class EditorStore extends BaseStore<EditorStore> {
|
||||
if (history.includes(id)) history.splice(history.indexOf(id), 1);
|
||||
history.push(id);
|
||||
}
|
||||
|
||||
if (activeBlockId && session)
|
||||
this.updateSession(session.id, [session.type], {
|
||||
activeBlockId: activeBlockId
|
||||
});
|
||||
};
|
||||
|
||||
openDiffSession = async (noteId: string, sessionId: string) => {
|
||||
@@ -275,7 +284,7 @@ class EditorStore extends BaseStore<EditorStore> {
|
||||
if (!oldContent || !currentContent) return;
|
||||
|
||||
const label = getFormattedHistorySessionDate(session);
|
||||
useEditorStore.getState().addSession({
|
||||
this.addSession({
|
||||
type: "diff",
|
||||
id: session.id,
|
||||
note,
|
||||
@@ -298,14 +307,14 @@ class EditorStore extends BaseStore<EditorStore> {
|
||||
|
||||
openSession = async (
|
||||
noteOrId: string | Note | BaseTrashItem<Note>,
|
||||
force = false
|
||||
options: { force?: boolean; activeBlockId?: string } = {}
|
||||
): Promise<void> => {
|
||||
const { getSession } = this.get();
|
||||
const noteId = typeof noteOrId === "string" ? noteOrId : noteOrId.id;
|
||||
const session = getSession(noteId);
|
||||
|
||||
if (session && !force && !session.needsHydration) {
|
||||
return this.activateSession(noteId);
|
||||
if (session && !options.force && !session.needsHydration) {
|
||||
return this.activateSession(noteId, options.activeBlockId);
|
||||
}
|
||||
|
||||
if (session && session.id) await db.fs().cancel(session.id, "download");
|
||||
@@ -322,7 +331,8 @@ class EditorStore extends BaseStore<EditorStore> {
|
||||
type: "locked",
|
||||
id: note.id,
|
||||
note,
|
||||
preview: isPreview
|
||||
preview: isPreview,
|
||||
activeBlockId: options.activeBlockId
|
||||
});
|
||||
} else if (note.conflicted) {
|
||||
const content = note.contentId
|
||||
@@ -343,7 +353,7 @@ class EditorStore extends BaseStore<EditorStore> {
|
||||
dateResolved: Date.now()
|
||||
});
|
||||
}
|
||||
return this.openSession(note, true);
|
||||
return this.openSession(note, { ...options, force: true });
|
||||
}
|
||||
|
||||
this.addSession({
|
||||
@@ -351,7 +361,8 @@ class EditorStore extends BaseStore<EditorStore> {
|
||||
content: content,
|
||||
id: note.id,
|
||||
note,
|
||||
preview: isPreview
|
||||
preview: isPreview,
|
||||
activeBlockId: options.activeBlockId
|
||||
});
|
||||
} else {
|
||||
const content = note.contentId
|
||||
@@ -361,7 +372,7 @@ class EditorStore extends BaseStore<EditorStore> {
|
||||
if (content?.locked) {
|
||||
note.locked = true;
|
||||
await db.notes.add({ id: note.id, locked: true });
|
||||
return this.openSession(note, true);
|
||||
return this.openSession(note, { ...options, force: true });
|
||||
}
|
||||
|
||||
if (note.type === "trash") {
|
||||
@@ -369,14 +380,16 @@ class EditorStore extends BaseStore<EditorStore> {
|
||||
type: "deleted",
|
||||
note,
|
||||
id: note.id,
|
||||
content
|
||||
content,
|
||||
activeBlockId: options.activeBlockId
|
||||
});
|
||||
} else if (note.readonly) {
|
||||
this.addSession({
|
||||
type: "readonly",
|
||||
note,
|
||||
id: note.id,
|
||||
content
|
||||
content,
|
||||
activeBlockId: options.activeBlockId
|
||||
});
|
||||
} else {
|
||||
const attachmentsLength = await db.attachments
|
||||
@@ -391,7 +404,8 @@ class EditorStore extends BaseStore<EditorStore> {
|
||||
sessionId: `${Date.now()}`,
|
||||
attachmentsLength,
|
||||
content,
|
||||
preview: isPreview
|
||||
preview: isPreview,
|
||||
activeBlockId: options.activeBlockId
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
44
apps/web/src/utils/clipboard.ts
Normal file
44
apps/web/src/utils/clipboard.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
type Formats = {
|
||||
"text/html"?: string;
|
||||
"text/markdown"?: string;
|
||||
"text/plain": string;
|
||||
};
|
||||
const COPYABLE_FORMATS = ["text/html", "text/plain"] as const;
|
||||
export async function writeToClipboard(formats: Formats) {
|
||||
if ("ClipboardItem" in window) {
|
||||
const items: Record<string, Blob> = Object.fromEntries(
|
||||
COPYABLE_FORMATS.map((f) => {
|
||||
const content = formats[f];
|
||||
if (!content) return [];
|
||||
return [f as string, textToBlob(content, f)] as const;
|
||||
})
|
||||
);
|
||||
return navigator.clipboard.write([new ClipboardItem(items)]);
|
||||
} else
|
||||
return navigator.clipboard.writeText(
|
||||
formats["text/markdown"] || formats["text/plain"]
|
||||
);
|
||||
}
|
||||
|
||||
function textToBlob(text: string, type: string) {
|
||||
return new Blob([new TextEncoder().encode(text)], { type });
|
||||
}
|
||||
Reference in New Issue
Block a user