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 */
|
/* 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%",
|
||||||
|
|||||||
5
packages/core/package-lock.json
generated
5
packages/core/package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user