mobile: note linking

This commit is contained in:
Ammar Ahmed
2024-03-07 11:15:16 +05:00
committed by Abdullah Atta
parent 270cb16d30
commit cfa98578ea
11 changed files with 490 additions and 135 deletions

View 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 />
});
};

View File

@@ -18,6 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
/* eslint-disable @typescript-eslint/no-var-requires */ /* eslint-disable @typescript-eslint/no-var-requires */
import { useThemeColors } from "@notesnook/theme"; import { useThemeColors } from "@notesnook/theme";
import React, { import React, {
forwardRef, forwardRef,
@@ -29,13 +30,9 @@ import React, {
useState useState
} from "react"; } from "react";
import { import {
Dimensions,
Keyboard,
Platform, Platform,
ScrollView, ScrollView,
TextInput, TextInput,
TouchableOpacity,
View,
ViewStyle, ViewStyle,
useWindowDimensions useWindowDimensions
} from "react-native"; } from "react-native";
@@ -43,15 +40,23 @@ import WebView from "react-native-webview";
import { ShouldStartLoadRequest } from "react-native-webview/lib/WebViewTypes"; import { ShouldStartLoadRequest } from "react-native-webview/lib/WebViewTypes";
import { notesnook } from "../../../e2e/test.ids"; import { notesnook } from "../../../e2e/test.ids";
import { db } from "../../common/database"; import { db } from "../../common/database";
import { Button } from "../../components/ui/button";
import { IconButton } from "../../components/ui/icon-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 useKeyboard from "../../hooks/use-keyboard";
import BiometicService from "../../services/biometrics";
import { import {
ToastManager, ToastManager,
eSendEvent, eSendEvent,
eSubscribeEvent, eSubscribeEvent
openVault
} from "../../services/event-manager"; } from "../../services/event-manager";
import { getElevationStyle } from "../../utils/elevation"; import { getElevationStyle } from "../../utils/elevation";
import { eOnLoadNote, eUnlockNote } from "../../utils/events";
import { openLinkInBrowser } from "../../utils/functions"; import { openLinkInBrowser } from "../../utils/functions";
import EditorOverlay from "./loading"; import EditorOverlay from "./loading";
import { EDITOR_URI } from "./source"; import { EDITOR_URI } from "./source";
@@ -60,20 +65,6 @@ import { useEditor } from "./tiptap/use-editor";
import { useEditorEvents } from "./tiptap/use-editor-events"; import { useEditorEvents } from "./tiptap/use-editor-events";
import { useTabStore } from "./tiptap/use-tab-store"; import { useTabStore } from "./tiptap/use-tab-store";
import { editorController, editorState } from "./tiptap/utils"; 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 = { const style: ViewStyle = {
height: "100%", height: "100%",

View File

@@ -17,6 +17,9 @@
"@streetwriters/showdown": "^3.0.5-alpha", "@streetwriters/showdown": "^3.0.5-alpha",
"async-mutex": "^0.3.2", "async-mutex": "^0.3.2",
"dayjs": "1.11.9", "dayjs": "1.11.9",
"dom-serializer": "^2.0.0",
"domhandler": "^5.0.3",
"domutils": "^3.1.0",
"entities": "^4.3.1", "entities": "^4.3.1",
"fuzzyjs": "^5.0.1", "fuzzyjs": "^5.0.1",
"html-to-text": "^9.0.5", "html-to-text": "^9.0.5",
@@ -2932,6 +2935,8 @@
}, },
"node_modules/domutils": { "node_modules/domutils": {
"version": "3.1.0", "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", "license": "BSD-2-Clause",
"dependencies": { "dependencies": {
"dom-serializer": "^2.0.0", "dom-serializer": "^2.0.0",

View File

@@ -55,6 +55,9 @@
"@streetwriters/showdown": "^3.0.5-alpha", "@streetwriters/showdown": "^3.0.5-alpha",
"async-mutex": "^0.3.2", "async-mutex": "^0.3.2",
"dayjs": "1.11.9", "dayjs": "1.11.9",
"dom-serializer": "^2.0.0",
"domhandler": "^5.0.3",
"domutils": "^3.1.0",
"entities": "^4.3.1", "entities": "^4.3.1",
"fuzzyjs": "^5.0.1", "fuzzyjs": "^5.0.1",
"html-to-text": "^9.0.5", "html-to-text": "^9.0.5",

View File

@@ -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) { function getNoteHeadline(content: Tiptap) {

View File

@@ -18,10 +18,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import showdown from "@streetwriters/showdown"; import showdown from "@streetwriters/showdown";
import dataurl from "../utils/dataurl"; import render from "dom-serializer";
import { extractFirstParagraph, getDummyDocument } from "../utils/html-parser"; import { find, isTag } from "domutils";
import { HTMLRewriter } from "../utils/html-rewriter";
import { HTMLParser } from "../utils/html-parser";
import { import {
DomNode, DomNode,
FormatOptions, FormatOptions,
@@ -29,6 +27,15 @@ import {
convert convert
} from "html-to-text"; } from "html-to-text";
import { BlockTextBuilder } from "html-to-text/lib/block-text-builder"; 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 = ( export type ResolveHashes = (
hashes: string[] hashes: string[]
@@ -38,7 +45,8 @@ const ATTRIBUTES = {
hash: "data-hash", hash: "data-hash",
mime: "data-mime", mime: "data-mime",
filename: "data-filename", filename: "data-filename",
src: "src" src: "src",
blockId: "data-block-id"
}; };
(showdown.helper as any).document = getDummyDocument(); (showdown.helper as any).document = getDummyDocument();
@@ -54,39 +62,7 @@ export class Tiptap {
} }
toTXT() { toTXT() {
return convert(this.data, { return convertHtmlToTxt(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
});
}
}
}
});
} }
toMD() { toMD() {
@@ -130,6 +106,30 @@ export class Tiptap {
}).transform(this.data); }).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 * @param {string[]} hashes
* @returns * @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( function formatList(
elem: DomNode, elem: DomNode,
walk: RecursiveCallback, walk: RecursiveCallback,

View File

@@ -550,3 +550,9 @@ export function isTrashItem(item: any): item is TrashItem {
export function isGroupHeader(item: any): item is GroupHeader { export function isGroupHeader(item: any): item is GroupHeader {
return item.type === "header"; return item.type === "header";
} }
export type ContentBlock = {
content: string;
type: string;
id: string;
};

View File

@@ -23,13 +23,13 @@ import {
themeToCSS, themeToCSS,
useThemeEngineStore useThemeEngineStore
} from "@notesnook/theme"; } from "@notesnook/theme";
import { useEffect, useMemo } from "react"; import { useMemo } from "react";
import { Freeze } from "react-freeze"; import { Freeze } from "react-freeze";
import "./App.css"; import "./App.css";
import Tiptap from "./components/editor"; import Tiptap from "./components/editor";
import { TabContext, useTabStore } from "./hooks/useTabStore"; import { TabContext, useTabStore } from "./hooks/useTabStore";
import { EmotionEditorTheme } from "./theme-factory"; import { EmotionEditorTheme } from "./theme-factory";
import { EventTypes, getTheme } from "./utils"; import { getTheme } from "./utils";
const currentTheme = getTheme(); const currentTheme = getTheme();
if (currentTheme) { if (currentTheme) {
@@ -39,7 +39,6 @@ if (currentTheme) {
function App(): JSX.Element { function App(): JSX.Element {
const tabs = useTabStore((state) => state.tabs); const tabs = useTabStore((state) => state.tabs);
const currentTab = useTabStore((state) => state.currentTab); const currentTab = useTabStore((state) => state.currentTab);
return ( return (
<ScopedThemeProvider value="base"> <ScopedThemeProvider value="base">
<EmotionEditorTheme> <EmotionEditorTheme>

View File

@@ -22,20 +22,14 @@ import {
getFontById, getFontById,
getTableOfContents, getTableOfContents,
PortalProvider, PortalProvider,
TiptapOptions,
Toolbar, Toolbar,
usePermissionHandler, usePermissionHandler,
useTiptap useTiptap
} from "@notesnook/editor"; } from "@notesnook/editor";
import { toBlobURL } from "@notesnook/editor/dist/utils/downloader"; import { toBlobURL } from "@notesnook/editor/dist/utils/downloader";
import { useThemeColors } from "@notesnook/theme"; import { useThemeColors } from "@notesnook/theme";
import { import { useCallback, useLayoutEffect, useMemo, useRef, useState } from "react";
forwardRef,
memo,
useCallback,
useLayoutEffect,
useRef,
useState
} from "react";
import { useEditorController } from "../hooks/useEditorController"; import { useEditorController } from "../hooks/useEditorController";
import { useSettings } from "../hooks/useSettings"; import { useSettings } from "../hooks/useSettings";
import { import {
@@ -53,14 +47,19 @@ import Title from "./title";
globalThis.toBlobURL = toBlobURL as typeof globalThis.toBlobURL; 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 { colors } = useThemeColors();
const tab = useTabContext(); const tab = useTabContext();
const isFocused = useTabStore((state) => state.currentTab === tab?.id); const isFocused = useTabStore((state) => state.currentTab === tab?.id);
const [tick, setTick] = useState(0); const [tick, setTick] = useState(0);
const contentRef = useRef<HTMLDivElement>(null);
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const [layout, setLayout] = useState(false);
const noteStateUpdateTimer = useRef<NodeJS.Timeout>(); const noteStateUpdateTimer = useRef<NodeJS.Timeout>();
const tabRef = useRef<TabItem>(tab); const tabRef = useRef<TabItem>(tab);
const isFocusedRef = useRef<boolean>(false); const isFocusedRef = useRef<boolean>(false);
@@ -71,12 +70,12 @@ const Tiptap = ({ settings }: { settings: Settings }) => {
premium: settings.premium premium: settings.premium
}, },
onPermissionDenied: () => { 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 }) => { onUpdate: ({ editor, transaction }) => {
globalThis.editorControllers[tab.id]?.contentChange( globalThis.editorControllers[tab.id]?.contentChange(
editor as Editor, editor as Editor,
@@ -100,7 +99,7 @@ const Tiptap = ({ settings }: { settings: Settings }) => {
attachment attachment
) as Promise<string | undefined>; ) as Promise<string | undefined>;
}, },
element: !layout ? undefined : contentRef.current || undefined, element: getContentDiv(),
editable: !settings.readonly, editable: !settings.readonly,
editorProps: { editorProps: {
editable: () => !settings.readonly editable: () => !settings.readonly
@@ -115,11 +114,11 @@ const Tiptap = ({ settings }: { settings: Settings }) => {
globalThis.editorControllers[tab.id]?.copyToClipboard(text); globalThis.editorControllers[tab.id]?.copyToClipboard(text);
}, },
onSelectionUpdate: () => { onSelectionUpdate: () => {
if (tab.noteId) { if (tabRef.current.noteId) {
const noteId = tab.noteId; const noteId = tabRef.current.noteId;
clearTimeout(noteStateUpdateTimer.current); clearTimeout(noteStateUpdateTimer.current);
noteStateUpdateTimer.current = setTimeout(() => { noteStateUpdateTimer.current = setTimeout(() => {
if (tab.noteId !== noteId) return; if (tabRef.current.noteId !== noteId) return;
const { to, from } = const { to, from } =
editors[tabRef.current?.id]?.state.selection || {}; editors[tabRef.current?.id]?.state.selection || {};
useTabStore.getState().setNoteState(noteId, { useTabStore.getState().setNoteState(noteId, {
@@ -135,17 +134,22 @@ const Tiptap = ({ settings }: { settings: Settings }) => {
dateFormat: settings.dateFormat, dateFormat: settings.dateFormat,
timeFormat: settings.timeFormat as "12-hour" | "24-hour" | undefined, timeFormat: settings.timeFormat as "12-hour" | "24-hour" | undefined,
enableInputRules: settings.markdownShortcuts enableInputRules: settings.markdownShortcuts
}, };
[ }, [
layout, getContentDiv,
settings.readonly, settings.readonly,
tick, settings.doubleSpacedLines,
settings.doubleSpacedLines, settings.corsProxy,
settings.markdownShortcuts settings.dateFormat,
] settings.timeFormat,
); settings.markdownShortcuts,
tab.id
]);
const _editor = useTiptap(tiptapOptions, [tiptapOptions]);
const update = useCallback(() => { const update = useCallback(() => {
logger("info", "update content");
setTick((tick) => tick + 1); setTick((tick) => tick + 1);
setTimeout(() => { setTimeout(() => {
const noteState = tabRef.current.noteId const noteState = tabRef.current.noteId
@@ -189,7 +193,10 @@ const Tiptap = ({ settings }: { settings: Settings }) => {
globalThis.editors[tab.id] = _editor; globalThis.editors[tab.id] = _editor;
useLayoutEffect(() => { useLayoutEffect(() => {
setLayout(true); if (!getContentDiv().parentElement) {
contentPlaceholderRef.current?.appendChild(getContentDiv());
}
const updateScrollPosition = (state: TabStore) => { const updateScrollPosition = (state: TabStore) => {
if (isFocusedRef.current) return; if (isFocusedRef.current) return;
if (state.currentTab === tabRef.current.id) { if (state.currentTab === tabRef.current.id) {
@@ -396,12 +403,7 @@ const Tiptap = ({ settings }: { settings: Settings }) => {
</div> </div>
) : null} ) : null}
<ContentDiv <div ref={contentPlaceholderRef} className="theme-scope-editor" />
padding={settings.doubleSpacedLines ? 0 : 6}
fontSize={settings.fontSize}
fontFamily={settings.fontFamily}
ref={contentRef}
/>
<div <div
onClick={(e) => { onClick={(e) => {
@@ -420,14 +422,15 @@ const Tiptap = ({ settings }: { settings: Settings }) => {
/> />
</div> </div>
{!layout || tab.locked ? null : ( {tab.locked ? null : (
<EmotionEditorToolbarTheme> <EmotionEditorToolbarTheme>
<Toolbar <Toolbar
className="theme-scope-editorToolbar" className="theme-scope-editorToolbar"
sx={{ sx={{
display: settings.noToolbar ? "none" : "flex", display: settings.noToolbar ? "none" : "flex",
overflowY: "hidden", overflowY: "hidden",
minHeight: "50px" minHeight: "50px",
backgroundColor: "red"
}} }}
editor={_editor} editor={_editor}
location="bottom" 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 TiptapProvider = (): JSX.Element => {
const settings = useSettings(); const settings = useSettings();
const { colors } = useThemeColors("editor");
const contentRef = useRef<HTMLElement>();
return ( return (
<PortalProvider> <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> </PortalProvider>
); );
}; };

View File

@@ -29,9 +29,5 @@ import "@notesnook/editor/styles/styles.css";
const rootElement = document.getElementById("root"); const rootElement = document.getElementById("root");
if (rootElement) { if (rootElement) {
const root = createRoot(rootElement); const root = createRoot(rootElement);
root.render( root.render(<App />);
<React.StrictMode>
<App />
</React.StrictMode>
);
} }

View File

@@ -39,7 +39,7 @@ export const useEditor = (
) => { ) => {
const editor = useMemo<Editor>(() => { const editor = useMemo<Editor>(() => {
const instance = new Editor(options); const instance = new Editor(options);
if (instance && !Object.hasOwn(instance, "current")) { if (instance && typeof instance.current === "undefined") {
Object.defineProperty(instance, "current", { Object.defineProperty(instance, "current", {
get: () => editorRef.current get: () => editorRef.current
}); });
@@ -59,7 +59,10 @@ export const useEditor = (
// than creating a new editor // than creating a new editor
// This part below is copied from @tiptap/core // This part below is copied from @tiptap/core
if (options.editorProps) editor.view.setProps(options.editorProps); 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( const doc = createDocument(
options.content, options.content,
editor.schema, editor.schema,