mirror of
https://github.com/streetwriters/notesnook.git
synced 2025-12-16 19:57:52 +01:00
mobile: note linking
This commit is contained in:
committed by
Abdullah Atta
parent
270cb16d30
commit
cfa98578ea
307
apps/mobile/app/components/sheets/link-note/index.tsx
Normal file
307
apps/mobile/app/components/sheets/link-note/index.tsx
Normal file
@@ -0,0 +1,307 @@
|
||||
/*
|
||||
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 { ContentBlock, Note, VirtualizedGrouping } from "@notesnook/core";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { TextInput, View } from "react-native";
|
||||
import { FlatList } from "react-native-actions-sheet";
|
||||
import { db } from "../../../common/database";
|
||||
import { useDBItem } from "../../../hooks/use-db-item";
|
||||
import { presentSheet } from "../../../services/event-manager";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import Input from "../../ui/input";
|
||||
import { PressableButton } from "../../ui/pressable";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { Button } from "../../ui/button";
|
||||
|
||||
const ListNoteItem = ({
|
||||
id,
|
||||
items,
|
||||
onSelectNote
|
||||
}: {
|
||||
id: any;
|
||||
items: VirtualizedGrouping<Note> | undefined;
|
||||
onSelectNote: any;
|
||||
}) => {
|
||||
const [item] = useDBItem(id, "note", items);
|
||||
return (
|
||||
<PressableButton
|
||||
onPress={() => {
|
||||
if (!item) return;
|
||||
onSelectNote(item as Note);
|
||||
}}
|
||||
type={"transparent"}
|
||||
customStyle={{
|
||||
paddingVertical: 12,
|
||||
flexDirection: "row",
|
||||
width: "100%",
|
||||
justifyContent: "flex-start",
|
||||
height: 50
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexShrink: 1
|
||||
}}
|
||||
>
|
||||
<Paragraph numberOfLines={1}>{item?.title}</Paragraph>
|
||||
</View>
|
||||
</PressableButton>
|
||||
);
|
||||
};
|
||||
|
||||
const ListBlockItem = ({
|
||||
item,
|
||||
onSelectBlock
|
||||
}: {
|
||||
item: ContentBlock;
|
||||
onSelectBlock: any;
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
return (
|
||||
<PressableButton
|
||||
onPress={() => {
|
||||
onSelectBlock(item);
|
||||
}}
|
||||
type={"transparent"}
|
||||
customStyle={{
|
||||
flexDirection: "row",
|
||||
width: "100%",
|
||||
justifyContent: "flex-start",
|
||||
minHeight: 45
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
width: "100%",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
style={{
|
||||
flexShrink: 1
|
||||
}}
|
||||
>
|
||||
{item?.content.length > 200
|
||||
? item?.content.slice(0, 200) + "..."
|
||||
: item.content}
|
||||
</Paragraph>
|
||||
|
||||
<View
|
||||
style={{
|
||||
borderRadius: 5,
|
||||
backgroundColor: colors.secondary.background,
|
||||
width: 30,
|
||||
height: 30,
|
||||
alignItems: "center",
|
||||
justifyContent: "center"
|
||||
}}
|
||||
>
|
||||
<Paragraph color={colors.secondary.paragraph} size={SIZE.xs}>
|
||||
{item.type.toUpperCase()}
|
||||
</Paragraph>
|
||||
</View>
|
||||
</View>
|
||||
</PressableButton>
|
||||
);
|
||||
};
|
||||
|
||||
export default function LinkNote() {
|
||||
const { colors } = useThemeColors();
|
||||
const query = useRef<string>();
|
||||
const [notes, setNotes] = useState<VirtualizedGrouping<Note>>();
|
||||
const nodesRef = useRef<ContentBlock[]>([]);
|
||||
const [nodes, setNodes] = useState<ContentBlock[]>([]);
|
||||
|
||||
const inputRef = useRef<TextInput>();
|
||||
|
||||
const [selectedNote, setSelectedNote] = useState<Note>();
|
||||
const [selectedNodeId, setSelectedNodeId] = useState<string>();
|
||||
|
||||
useEffect(() => {
|
||||
db.notes.all.sorted(db.settings.getGroupOptions("notes")).then((notes) => {
|
||||
setNotes(notes);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const onChange = async (value: string) => {
|
||||
query.current = value;
|
||||
if (!selectedNote) {
|
||||
const notes = await db.lookup.notes(value).sorted();
|
||||
setNotes(notes);
|
||||
} else {
|
||||
if (value.startsWith("#")) {
|
||||
const headingNodes = nodesRef.current.filter((n) =>
|
||||
n.type.match(/(h1|h2|h3|h4|h5|h6)/g)
|
||||
);
|
||||
setNodes(
|
||||
headingNodes.filter((n) => n.content.includes(value.slice(1)))
|
||||
);
|
||||
} else {
|
||||
setNodes(nodesRef.current.filter((n) => n.content.includes(value)));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onSelectNote = async (note: Note) => {
|
||||
setSelectedNote(note);
|
||||
inputRef.current?.clear();
|
||||
setTimeout(async () => {
|
||||
nodesRef.current = await db.notes.getBlocks(note.id);
|
||||
setNodes(nodesRef.current);
|
||||
});
|
||||
// Fetch and set note's nodes.
|
||||
};
|
||||
|
||||
const onSelectBlock = (block: ContentBlock) => {
|
||||
setSelectedNodeId(block.id);
|
||||
};
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
height: "100%",
|
||||
flexShrink: 1,
|
||||
borderWidth: 2,
|
||||
borderColor: "red"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "column",
|
||||
width: "100%",
|
||||
alignItems: "flex-start",
|
||||
gap: 10
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
placeholder={
|
||||
selectedNote
|
||||
? "Search a section of a note to link to"
|
||||
: "Search a note to link to"
|
||||
}
|
||||
containerStyle={{
|
||||
width: "100%"
|
||||
}}
|
||||
marginBottom={0}
|
||||
onChangeText={(value) => {
|
||||
onChange(value);
|
||||
}}
|
||||
/>
|
||||
|
||||
{selectedNote ? (
|
||||
<View
|
||||
style={{
|
||||
gap: 10
|
||||
}}
|
||||
>
|
||||
<Paragraph color={colors.secondary.paragraph} size={SIZE.xs}>
|
||||
SELECTED NOTE
|
||||
</Paragraph>
|
||||
<PressableButton
|
||||
onPress={() => {
|
||||
setSelectedNote(undefined);
|
||||
setSelectedNodeId(undefined);
|
||||
setNodes([]);
|
||||
}}
|
||||
customStyle={{
|
||||
flexDirection: "row",
|
||||
width: "100%",
|
||||
justifyContent: "flex-start",
|
||||
height: 45,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.primary.accent,
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
type="grayAccent"
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<Paragraph numberOfLines={1}>{selectedNote?.title}</Paragraph>
|
||||
|
||||
<Paragraph color={colors.secondary.paragraph} size={SIZE.xs}>
|
||||
Tap to deselect
|
||||
</Paragraph>
|
||||
</View>
|
||||
</PressableButton>
|
||||
|
||||
{nodes?.length > 0 ? (
|
||||
<Paragraph
|
||||
style={{
|
||||
marginBottom: 12
|
||||
}}
|
||||
color={colors.secondary.paragraph}
|
||||
size={SIZE.xs}
|
||||
>
|
||||
LINK TO A SECTION
|
||||
</Paragraph>
|
||||
) : null}
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
{selectedNote ? (
|
||||
<FlatList
|
||||
renderItem={({ item, index }) => (
|
||||
<ListBlockItem item={item} onSelectBlock={onSelectBlock} />
|
||||
)}
|
||||
keyExtractor={(item) => item.id}
|
||||
data={nodes}
|
||||
/>
|
||||
) : (
|
||||
<FlatList
|
||||
renderItem={({ item, index }: any) => (
|
||||
<ListNoteItem
|
||||
id={index}
|
||||
items={notes}
|
||||
onSelectNote={onSelectNote}
|
||||
/>
|
||||
)}
|
||||
data={notes?.placeholders}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedNote ? (
|
||||
<Button
|
||||
style={{
|
||||
marginTop: 10
|
||||
}}
|
||||
title="Create link"
|
||||
type="accent"
|
||||
width="100%"
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
LinkNote.present = () => {
|
||||
presentSheet({
|
||||
component: () => <LinkNote />
|
||||
});
|
||||
};
|
||||
@@ -18,6 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, {
|
||||
forwardRef,
|
||||
@@ -29,13 +30,9 @@ import React, {
|
||||
useState
|
||||
} from "react";
|
||||
import {
|
||||
Dimensions,
|
||||
Keyboard,
|
||||
Platform,
|
||||
ScrollView,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
ViewStyle,
|
||||
useWindowDimensions
|
||||
} from "react-native";
|
||||
@@ -43,15 +40,23 @@ import WebView from "react-native-webview";
|
||||
import { ShouldStartLoadRequest } from "react-native-webview/lib/WebViewTypes";
|
||||
import { notesnook } from "../../../e2e/test.ids";
|
||||
import { db } from "../../common/database";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import { IconButton } from "../../components/ui/icon-button";
|
||||
import Input from "../../components/ui/input";
|
||||
import Seperator from "../../components/ui/seperator";
|
||||
import Heading from "../../components/ui/typography/heading";
|
||||
import Paragraph from "../../components/ui/typography/paragraph";
|
||||
import { useDBItem } from "../../hooks/use-db-item";
|
||||
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
|
||||
import useKeyboard from "../../hooks/use-keyboard";
|
||||
import BiometicService from "../../services/biometrics";
|
||||
import {
|
||||
ToastManager,
|
||||
eSendEvent,
|
||||
eSubscribeEvent,
|
||||
openVault
|
||||
eSubscribeEvent
|
||||
} from "../../services/event-manager";
|
||||
import { getElevationStyle } from "../../utils/elevation";
|
||||
import { eOnLoadNote, eUnlockNote } from "../../utils/events";
|
||||
import { openLinkInBrowser } from "../../utils/functions";
|
||||
import EditorOverlay from "./loading";
|
||||
import { EDITOR_URI } from "./source";
|
||||
@@ -60,20 +65,6 @@ import { useEditor } from "./tiptap/use-editor";
|
||||
import { useEditorEvents } from "./tiptap/use-editor-events";
|
||||
import { useTabStore } from "./tiptap/use-tab-store";
|
||||
import { editorController, editorState } from "./tiptap/utils";
|
||||
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import Heading from "../../components/ui/typography/heading";
|
||||
import Seperator from "../../components/ui/seperator";
|
||||
import Paragraph from "../../components/ui/typography/paragraph";
|
||||
import { useDBItem } from "../../hooks/use-db-item";
|
||||
import Input from "../../components/ui/input";
|
||||
import BiometicService from "../../services/biometrics";
|
||||
import { eOnLoadNote, eUnlockNote } from "../../utils/events";
|
||||
import Menu, {
|
||||
MenuItem,
|
||||
MenuDivider
|
||||
} from "react-native-reanimated-material-menu";
|
||||
|
||||
const style: ViewStyle = {
|
||||
height: "100%",
|
||||
|
||||
5
packages/core/package-lock.json
generated
5
packages/core/package-lock.json
generated
@@ -17,6 +17,9 @@
|
||||
"@streetwriters/showdown": "^3.0.5-alpha",
|
||||
"async-mutex": "^0.3.2",
|
||||
"dayjs": "1.11.9",
|
||||
"dom-serializer": "^2.0.0",
|
||||
"domhandler": "^5.0.3",
|
||||
"domutils": "^3.1.0",
|
||||
"entities": "^4.3.1",
|
||||
"fuzzyjs": "^5.0.1",
|
||||
"html-to-text": "^9.0.5",
|
||||
@@ -2932,6 +2935,8 @@
|
||||
},
|
||||
"node_modules/domutils": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz",
|
||||
"integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"dom-serializer": "^2.0.0",
|
||||
|
||||
@@ -55,6 +55,9 @@
|
||||
"@streetwriters/showdown": "^3.0.5-alpha",
|
||||
"async-mutex": "^0.3.2",
|
||||
"dayjs": "1.11.9",
|
||||
"dom-serializer": "^2.0.0",
|
||||
"domhandler": "^5.0.3",
|
||||
"domutils": "^3.1.0",
|
||||
"entities": "^4.3.1",
|
||||
"fuzzyjs": "^5.0.1",
|
||||
"html-to-text": "^9.0.5",
|
||||
|
||||
@@ -394,6 +394,18 @@ export class Notes implements ICollection {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async getBlocks(id: string) {
|
||||
const note = await this.collection.get(id);
|
||||
if (note?.locked || !note?.contentId) return [];
|
||||
const rawContent = await this.db.content.get(note.contentId);
|
||||
if (!rawContent || rawContent.locked) return [];
|
||||
|
||||
return getContentFromData(
|
||||
rawContent.type,
|
||||
rawContent?.data
|
||||
).extractBlocks();
|
||||
}
|
||||
}
|
||||
|
||||
function getNoteHeadline(content: Tiptap) {
|
||||
|
||||
@@ -18,10 +18,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import showdown from "@streetwriters/showdown";
|
||||
import dataurl from "../utils/dataurl";
|
||||
import { extractFirstParagraph, getDummyDocument } from "../utils/html-parser";
|
||||
import { HTMLRewriter } from "../utils/html-rewriter";
|
||||
import { HTMLParser } from "../utils/html-parser";
|
||||
import render from "dom-serializer";
|
||||
import { find, isTag } from "domutils";
|
||||
import {
|
||||
DomNode,
|
||||
FormatOptions,
|
||||
@@ -29,6 +27,15 @@ import {
|
||||
convert
|
||||
} from "html-to-text";
|
||||
import { BlockTextBuilder } from "html-to-text/lib/block-text-builder";
|
||||
import { parseDocument } from "htmlparser2";
|
||||
import dataurl from "../utils/dataurl";
|
||||
import {
|
||||
HTMLParser,
|
||||
extractFirstParagraph,
|
||||
getDummyDocument
|
||||
} from "../utils/html-parser";
|
||||
import { HTMLRewriter } from "../utils/html-rewriter";
|
||||
import { ContentBlock } from "../types";
|
||||
|
||||
export type ResolveHashes = (
|
||||
hashes: string[]
|
||||
@@ -38,7 +45,8 @@ const ATTRIBUTES = {
|
||||
hash: "data-hash",
|
||||
mime: "data-mime",
|
||||
filename: "data-filename",
|
||||
src: "src"
|
||||
src: "src",
|
||||
blockId: "data-block-id"
|
||||
};
|
||||
|
||||
(showdown.helper as any).document = getDummyDocument();
|
||||
@@ -54,39 +62,7 @@ export class Tiptap {
|
||||
}
|
||||
|
||||
toTXT() {
|
||||
return convert(this.data, {
|
||||
wordwrap: 80,
|
||||
preserveNewlines: true,
|
||||
selectors: [
|
||||
{ selector: "table", format: "dataTable" },
|
||||
{ selector: "ul.checklist", format: "checkList" },
|
||||
{ selector: "ul.simple-checklist", format: "checkList" },
|
||||
{ selector: "p", format: "paragraph" }
|
||||
],
|
||||
formatters: {
|
||||
checkList: (elem, walk, builder, formatOptions) => {
|
||||
return formatList(elem, walk, builder, formatOptions, (elem) => {
|
||||
return elem.attribs.class && elem.attribs.class.includes("checked")
|
||||
? " ✅ "
|
||||
: " ☐ ";
|
||||
});
|
||||
},
|
||||
paragraph: (elem, walk, builder) => {
|
||||
const { "data-spacing": dataSpacing } = elem.attribs;
|
||||
if (elem.parent && elem.parent.name === "li") {
|
||||
walk(elem.children, builder);
|
||||
} else {
|
||||
builder.openBlock({
|
||||
leadingLineBreaks: dataSpacing == "single" ? 1 : 2
|
||||
});
|
||||
walk(elem.children, builder);
|
||||
builder.closeBlock({
|
||||
trailingLineBreaks: 1
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return convertHtmlToTxt(this.data);
|
||||
}
|
||||
|
||||
toMD() {
|
||||
@@ -130,6 +106,30 @@ export class Tiptap {
|
||||
}).transform(this.data);
|
||||
}
|
||||
|
||||
async extractBlocks() {
|
||||
const nodes: ContentBlock[] = [];
|
||||
const document = parseDocument(this.data);
|
||||
|
||||
const elements = find(
|
||||
(element) => {
|
||||
return isTag(element) && !!element.attribs[ATTRIBUTES.blockId];
|
||||
},
|
||||
document.childNodes,
|
||||
false,
|
||||
Infinity
|
||||
);
|
||||
|
||||
for (const node of elements) {
|
||||
if (!isTag(node)) continue;
|
||||
nodes.push({
|
||||
id: node.attribs[ATTRIBUTES.blockId],
|
||||
type: node.tagName.toLowerCase(),
|
||||
content: convertHtmlToTxt(render(node))
|
||||
});
|
||||
}
|
||||
return nodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string[]} hashes
|
||||
* @returns
|
||||
@@ -233,6 +233,42 @@ export class Tiptap {
|
||||
}
|
||||
}
|
||||
|
||||
function convertHtmlToTxt(html: string) {
|
||||
return convert(html, {
|
||||
wordwrap: 80,
|
||||
preserveNewlines: true,
|
||||
selectors: [
|
||||
{ selector: "table", format: "dataTable" },
|
||||
{ selector: "ul.checklist", format: "taskList" },
|
||||
{ selector: "ul.simple-checklist", format: "checkList" },
|
||||
{ selector: "p", format: "paragraph" }
|
||||
],
|
||||
formatters: {
|
||||
taskList: (elem, walk, builder, formatOptions) => {
|
||||
return formatList(elem, walk, builder, formatOptions, (elem) => {
|
||||
return elem.attribs.class && elem.attribs.class.includes("checked")
|
||||
? " ✅ "
|
||||
: " ☐ ";
|
||||
});
|
||||
},
|
||||
paragraph: (elem, walk, builder) => {
|
||||
const { "data-spacing": dataSpacing } = elem.attribs;
|
||||
if (elem.parent && elem.parent.name === "li") {
|
||||
walk(elem.children, builder);
|
||||
} else {
|
||||
builder.openBlock({
|
||||
leadingLineBreaks: dataSpacing == "single" ? 1 : 2
|
||||
});
|
||||
walk(elem.children, builder);
|
||||
builder.closeBlock({
|
||||
trailingLineBreaks: 1
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function formatList(
|
||||
elem: DomNode,
|
||||
walk: RecursiveCallback,
|
||||
|
||||
@@ -550,3 +550,9 @@ export function isTrashItem(item: any): item is TrashItem {
|
||||
export function isGroupHeader(item: any): item is GroupHeader {
|
||||
return item.type === "header";
|
||||
}
|
||||
|
||||
export type ContentBlock = {
|
||||
content: string;
|
||||
type: string;
|
||||
id: string;
|
||||
};
|
||||
|
||||
@@ -23,13 +23,13 @@ import {
|
||||
themeToCSS,
|
||||
useThemeEngineStore
|
||||
} from "@notesnook/theme";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { useMemo } from "react";
|
||||
import { Freeze } from "react-freeze";
|
||||
import "./App.css";
|
||||
import Tiptap from "./components/editor";
|
||||
import { TabContext, useTabStore } from "./hooks/useTabStore";
|
||||
import { EmotionEditorTheme } from "./theme-factory";
|
||||
import { EventTypes, getTheme } from "./utils";
|
||||
import { getTheme } from "./utils";
|
||||
|
||||
const currentTheme = getTheme();
|
||||
if (currentTheme) {
|
||||
@@ -39,7 +39,6 @@ if (currentTheme) {
|
||||
function App(): JSX.Element {
|
||||
const tabs = useTabStore((state) => state.tabs);
|
||||
const currentTab = useTabStore((state) => state.currentTab);
|
||||
|
||||
return (
|
||||
<ScopedThemeProvider value="base">
|
||||
<EmotionEditorTheme>
|
||||
|
||||
@@ -22,20 +22,14 @@ import {
|
||||
getFontById,
|
||||
getTableOfContents,
|
||||
PortalProvider,
|
||||
TiptapOptions,
|
||||
Toolbar,
|
||||
usePermissionHandler,
|
||||
useTiptap
|
||||
} from "@notesnook/editor";
|
||||
import { toBlobURL } from "@notesnook/editor/dist/utils/downloader";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import {
|
||||
forwardRef,
|
||||
memo,
|
||||
useCallback,
|
||||
useLayoutEffect,
|
||||
useRef,
|
||||
useState
|
||||
} from "react";
|
||||
import { useCallback, useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||
import { useEditorController } from "../hooks/useEditorController";
|
||||
import { useSettings } from "../hooks/useSettings";
|
||||
import {
|
||||
@@ -53,14 +47,19 @@ import Title from "./title";
|
||||
|
||||
globalThis.toBlobURL = toBlobURL as typeof globalThis.toBlobURL;
|
||||
|
||||
const Tiptap = ({ settings }: { settings: Settings }) => {
|
||||
const Tiptap = ({
|
||||
settings,
|
||||
getContentDiv
|
||||
}: {
|
||||
settings: Settings;
|
||||
getContentDiv: () => HTMLElement;
|
||||
}) => {
|
||||
const contentPlaceholderRef = useRef<HTMLDivElement>(null);
|
||||
const { colors } = useThemeColors();
|
||||
const tab = useTabContext();
|
||||
const isFocused = useTabStore((state) => state.currentTab === tab?.id);
|
||||
const [tick, setTick] = useState(0);
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [layout, setLayout] = useState(false);
|
||||
const noteStateUpdateTimer = useRef<NodeJS.Timeout>();
|
||||
const tabRef = useRef<TabItem>(tab);
|
||||
const isFocusedRef = useRef<boolean>(false);
|
||||
@@ -71,12 +70,12 @@ const Tiptap = ({ settings }: { settings: Settings }) => {
|
||||
premium: settings.premium
|
||||
},
|
||||
onPermissionDenied: () => {
|
||||
post(EventTypes.pro, undefined, tab.id, tab.noteId);
|
||||
post(EventTypes.pro, undefined, tabRef.current.id, tab.noteId);
|
||||
}
|
||||
});
|
||||
|
||||
const _editor = useTiptap(
|
||||
{
|
||||
const tiptapOptions = useMemo<Partial<TiptapOptions>>(() => {
|
||||
return {
|
||||
onUpdate: ({ editor, transaction }) => {
|
||||
globalThis.editorControllers[tab.id]?.contentChange(
|
||||
editor as Editor,
|
||||
@@ -100,7 +99,7 @@ const Tiptap = ({ settings }: { settings: Settings }) => {
|
||||
attachment
|
||||
) as Promise<string | undefined>;
|
||||
},
|
||||
element: !layout ? undefined : contentRef.current || undefined,
|
||||
element: getContentDiv(),
|
||||
editable: !settings.readonly,
|
||||
editorProps: {
|
||||
editable: () => !settings.readonly
|
||||
@@ -115,11 +114,11 @@ const Tiptap = ({ settings }: { settings: Settings }) => {
|
||||
globalThis.editorControllers[tab.id]?.copyToClipboard(text);
|
||||
},
|
||||
onSelectionUpdate: () => {
|
||||
if (tab.noteId) {
|
||||
const noteId = tab.noteId;
|
||||
if (tabRef.current.noteId) {
|
||||
const noteId = tabRef.current.noteId;
|
||||
clearTimeout(noteStateUpdateTimer.current);
|
||||
noteStateUpdateTimer.current = setTimeout(() => {
|
||||
if (tab.noteId !== noteId) return;
|
||||
if (tabRef.current.noteId !== noteId) return;
|
||||
const { to, from } =
|
||||
editors[tabRef.current?.id]?.state.selection || {};
|
||||
useTabStore.getState().setNoteState(noteId, {
|
||||
@@ -135,17 +134,22 @@ const Tiptap = ({ settings }: { settings: Settings }) => {
|
||||
dateFormat: settings.dateFormat,
|
||||
timeFormat: settings.timeFormat as "12-hour" | "24-hour" | undefined,
|
||||
enableInputRules: settings.markdownShortcuts
|
||||
},
|
||||
[
|
||||
layout,
|
||||
settings.readonly,
|
||||
tick,
|
||||
settings.doubleSpacedLines,
|
||||
settings.markdownShortcuts
|
||||
]
|
||||
);
|
||||
};
|
||||
}, [
|
||||
getContentDiv,
|
||||
settings.readonly,
|
||||
settings.doubleSpacedLines,
|
||||
settings.corsProxy,
|
||||
settings.dateFormat,
|
||||
settings.timeFormat,
|
||||
settings.markdownShortcuts,
|
||||
tab.id
|
||||
]);
|
||||
|
||||
const _editor = useTiptap(tiptapOptions, [tiptapOptions]);
|
||||
|
||||
const update = useCallback(() => {
|
||||
logger("info", "update content");
|
||||
setTick((tick) => tick + 1);
|
||||
setTimeout(() => {
|
||||
const noteState = tabRef.current.noteId
|
||||
@@ -189,7 +193,10 @@ const Tiptap = ({ settings }: { settings: Settings }) => {
|
||||
globalThis.editors[tab.id] = _editor;
|
||||
|
||||
useLayoutEffect(() => {
|
||||
setLayout(true);
|
||||
if (!getContentDiv().parentElement) {
|
||||
contentPlaceholderRef.current?.appendChild(getContentDiv());
|
||||
}
|
||||
|
||||
const updateScrollPosition = (state: TabStore) => {
|
||||
if (isFocusedRef.current) return;
|
||||
if (state.currentTab === tabRef.current.id) {
|
||||
@@ -396,12 +403,7 @@ const Tiptap = ({ settings }: { settings: Settings }) => {
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<ContentDiv
|
||||
padding={settings.doubleSpacedLines ? 0 : 6}
|
||||
fontSize={settings.fontSize}
|
||||
fontFamily={settings.fontFamily}
|
||||
ref={contentRef}
|
||||
/>
|
||||
<div ref={contentPlaceholderRef} className="theme-scope-editor" />
|
||||
|
||||
<div
|
||||
onClick={(e) => {
|
||||
@@ -420,14 +422,15 @@ const Tiptap = ({ settings }: { settings: Settings }) => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!layout || tab.locked ? null : (
|
||||
{tab.locked ? null : (
|
||||
<EmotionEditorToolbarTheme>
|
||||
<Toolbar
|
||||
className="theme-scope-editorToolbar"
|
||||
sx={{
|
||||
display: settings.noToolbar ? "none" : "flex",
|
||||
overflowY: "hidden",
|
||||
minHeight: "50px"
|
||||
minHeight: "50px",
|
||||
backgroundColor: "red"
|
||||
}}
|
||||
editor={_editor}
|
||||
location="bottom"
|
||||
@@ -442,41 +445,35 @@ const Tiptap = ({ settings }: { settings: Settings }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const ContentDiv = memo(
|
||||
forwardRef<
|
||||
HTMLDivElement,
|
||||
{ padding: number; fontSize: number; fontFamily: string }
|
||||
>((props, ref) => {
|
||||
const { colors } = useThemeColors("editor");
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className="theme-scope-editor"
|
||||
style={{
|
||||
padding: 12,
|
||||
paddingTop: props.padding,
|
||||
color: colors.primary.paragraph,
|
||||
marginTop: -12,
|
||||
caretColor: colors.primary.accent,
|
||||
fontSize: props.fontSize,
|
||||
fontFamily: getFontById(props.fontFamily)?.font
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}),
|
||||
(prev, next) => {
|
||||
if (prev.fontSize !== next.fontSize || prev.fontFamily !== next.fontFamily)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
);
|
||||
|
||||
const TiptapProvider = (): JSX.Element => {
|
||||
const settings = useSettings();
|
||||
|
||||
const { colors } = useThemeColors("editor");
|
||||
const contentRef = useRef<HTMLElement>();
|
||||
return (
|
||||
<PortalProvider>
|
||||
<Tiptap settings={settings} />
|
||||
<Tiptap
|
||||
settings={settings}
|
||||
getContentDiv={() => {
|
||||
if (contentRef.current) {
|
||||
logger("info", "return content");
|
||||
return contentRef.current;
|
||||
}
|
||||
logger("info", "new content");
|
||||
const editorContainer = document.createElement("div");
|
||||
editorContainer.classList.add("selectable");
|
||||
editorContainer.style.flex = "1";
|
||||
editorContainer.style.cursor = "text";
|
||||
editorContainer.style.padding = "0px 12px";
|
||||
editorContainer.style.color =
|
||||
colors?.primary?.paragraph || colors.primary.paragraph;
|
||||
editorContainer.style.paddingBottom = `150px`;
|
||||
editorContainer.style.fontSize = `${settings.fontSize}px`;
|
||||
editorContainer.style.fontFamily =
|
||||
getFontById(settings.fontFamily)?.font || "sans-serif";
|
||||
contentRef.current = editorContainer;
|
||||
return editorContainer;
|
||||
}}
|
||||
/>
|
||||
</PortalProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -29,9 +29,5 @@ import "@notesnook/editor/styles/styles.css";
|
||||
const rootElement = document.getElementById("root");
|
||||
if (rootElement) {
|
||||
const root = createRoot(rootElement);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
root.render(<App />);
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ export const useEditor = (
|
||||
) => {
|
||||
const editor = useMemo<Editor>(() => {
|
||||
const instance = new Editor(options);
|
||||
if (instance && !Object.hasOwn(instance, "current")) {
|
||||
if (instance && typeof instance.current === "undefined") {
|
||||
Object.defineProperty(instance, "current", {
|
||||
get: () => editorRef.current
|
||||
});
|
||||
@@ -59,7 +59,10 @@ export const useEditor = (
|
||||
// than creating a new editor
|
||||
// This part below is copied from @tiptap/core
|
||||
if (options.editorProps) editor.view.setProps(options.editorProps);
|
||||
if (options.content && options.content !== editor.options.content) {
|
||||
if (
|
||||
options.content !== undefined &&
|
||||
options.content !== editor.options.content
|
||||
) {
|
||||
const doc = createDocument(
|
||||
options.content,
|
||||
editor.schema,
|
||||
|
||||
Reference in New Issue
Block a user