mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-09-01 19:49:54 +02:00
web: add full support for adding internal links
This commit is contained in:
@@ -42,6 +42,7 @@ import {
|
||||
} from "@notesnook/core";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { PasswordDialogProps } from "../dialogs/password-dialog";
|
||||
import { LinkAttributes } from "@notesnook/editor/dist/extensions/link";
|
||||
|
||||
type DialogTypes = typeof Dialogs;
|
||||
type DialogIds = keyof DialogTypes;
|
||||
@@ -113,6 +114,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;
|
||||
|
||||
@@ -63,6 +63,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"));
|
||||
|
||||
@@ -408,6 +410,7 @@ export function Editor(props: EditorProps) {
|
||||
isMobile: false
|
||||
};
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
useScrollToBlock(id);
|
||||
|
||||
useEffect(() => {
|
||||
const event = AppEventManager.subscribe(
|
||||
@@ -503,6 +506,10 @@ export function Editor(props: EditorProps) {
|
||||
if (!result) return;
|
||||
result.forEach((attachment) => editor?.attachFile(attachment));
|
||||
}}
|
||||
onInsertInternalLink={async (attributes) => {
|
||||
const link = await showNoteLinkingDialog(attributes);
|
||||
return link;
|
||||
}}
|
||||
>
|
||||
{headless ? null : (
|
||||
<>
|
||||
@@ -805,6 +812,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 &&
|
||||
@@ -814,6 +831,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) {
|
||||
|
||||
@@ -59,10 +59,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,
|
||||
@@ -80,6 +83,10 @@ type TipTapProps = {
|
||||
onPreviewAttachment?: (attachment: Attachment) => void;
|
||||
onGetAttachmentData?: (attachment: Attachment) => Promise<string | undefined>;
|
||||
onAttachFiles?: (files: File[]) => void;
|
||||
onInsertInternalLink?: (
|
||||
attributes?: LinkAttributes
|
||||
) => Promise<LinkAttributes | undefined>;
|
||||
onAttachFile?: (file: File) => void;
|
||||
onFocus?: () => void;
|
||||
content?: () => string | undefined;
|
||||
readonly?: boolean;
|
||||
@@ -115,6 +122,7 @@ function TipTap(props: TipTapProps) {
|
||||
onPreviewAttachment,
|
||||
onGetAttachmentData,
|
||||
onAttachFiles,
|
||||
onInsertInternalLink,
|
||||
onContentChange,
|
||||
onFocus = () => {},
|
||||
content,
|
||||
@@ -239,8 +247,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;
|
||||
@@ -269,23 +277,19 @@ 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;
|
||||
},
|
||||
getAttachmentData: onGetAttachmentData
|
||||
openAttachmentPicker: onInsertAttachment,
|
||||
downloadAttachment: onDownloadAttachment,
|
||||
previewAttachment: onPreviewAttachment,
|
||||
createInternalLink: onInsertInternalLink,
|
||||
getAttachmentData: onGetAttachmentData,
|
||||
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,
|
||||
@@ -429,17 +433,17 @@ 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(),
|
||||
updateContent: (content) => {
|
||||
const { from, to } = editor.state.selection;
|
||||
editor.current
|
||||
editor
|
||||
?.chain()
|
||||
.command(({ tr }) => {
|
||||
tr.setMeta("preventSave", true);
|
||||
@@ -454,10 +458,10 @@ function toIEditor(editor: Editor): IEditor {
|
||||
},
|
||||
attachFile: (file: Attachment) =>
|
||||
file.type === "image"
|
||||
? editor.current?.commands.insertImage(file)
|
||||
: editor.current?.commands.insertAttachment(file),
|
||||
? editor.commands.insertImage(file)
|
||||
: editor.commands.insertAttachment(file),
|
||||
sendAttachmentProgress: (hash, progress) =>
|
||||
editor.current?.commands.updateAttachment(
|
||||
editor.commands.updateAttachment(
|
||||
{
|
||||
progress
|
||||
},
|
||||
|
||||
@@ -210,7 +210,8 @@ import {
|
||||
mdiBellBadgeOutline,
|
||||
mdiDotsHorizontal,
|
||||
mdiCalendarBlank,
|
||||
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,
|
||||
@@ -60,7 +61,7 @@ import {
|
||||
showCreateColorDialog,
|
||||
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";
|
||||
@@ -82,11 +83,19 @@ import {
|
||||
getFormattedReminderTime,
|
||||
pluralize
|
||||
} from "@notesnook/common";
|
||||
import { Color, Note, Notebook as NotebookItem, Tag } from "@notesnook/core";
|
||||
import {
|
||||
Color,
|
||||
Note,
|
||||
Notebook as NotebookItem,
|
||||
Tag,
|
||||
createInternalLink
|
||||
} from "@notesnook/core";
|
||||
import { MenuItem } from "@notesnook/ui";
|
||||
import { Context } from "../list-container/types";
|
||||
import { SchemeColors } from "@notesnook/theme";
|
||||
import FileSaver from "file-saver";
|
||||
import Vault from "../../common/vault";
|
||||
import { writeToClipboard } from "../../utils/clipboard";
|
||||
|
||||
type NoteProps = NoteResolvedData & {
|
||||
item: Note;
|
||||
@@ -497,6 +506,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",
|
||||
|
||||
@@ -60,6 +60,7 @@ const EditProfilePictureDialog = React.lazy(
|
||||
() => import("./edit-profile-picture-dialog")
|
||||
);
|
||||
const ImagePickerDialog = React.lazy(() => import("./image-picker-dialog"));
|
||||
const NoteLinkingDialog = React.lazy(() => import("./note-linking-dialog"));
|
||||
|
||||
export const Dialogs = {
|
||||
AddNotebookDialog,
|
||||
@@ -91,5 +92,6 @@ export const Dialogs = {
|
||||
BackupPasswordDialog,
|
||||
CreateColorDialog,
|
||||
EditProfilePictureDialog,
|
||||
ImagePickerDialog
|
||||
ImagePickerDialog,
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -59,6 +59,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 & {
|
||||
@@ -199,7 +204,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);
|
||||
@@ -243,7 +247,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;
|
||||
|
||||
@@ -265,6 +269,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) => {
|
||||
@@ -301,14 +310,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);
|
||||
@@ -325,7 +334,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
|
||||
@@ -346,7 +356,7 @@ class EditorStore extends BaseStore<EditorStore> {
|
||||
dateResolved: Date.now()
|
||||
});
|
||||
}
|
||||
return this.openSession(note, true);
|
||||
return this.openSession(note, { ...options, force: true });
|
||||
}
|
||||
|
||||
this.addSession({
|
||||
@@ -354,7 +364,8 @@ class EditorStore extends BaseStore<EditorStore> {
|
||||
content: content,
|
||||
id: note.id,
|
||||
note,
|
||||
preview: isPreview
|
||||
preview: isPreview,
|
||||
activeBlockId: options.activeBlockId
|
||||
});
|
||||
} else {
|
||||
const content = note.contentId
|
||||
@@ -364,7 +375,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") {
|
||||
@@ -372,14 +383,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
|
||||
@@ -394,7 +407,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