mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-02-24 12:12:54 +01:00
mobile: implement toc
This commit is contained in:
125
apps/mobile/app/components/sheets/toc/index.tsx
Normal file
125
apps/mobile/app/components/sheets/toc/index.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React from "react";
|
||||
import { View } from "react-native";
|
||||
import { PressableButton } from "../../ui/pressable";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import { presentSheet } from "../../../services/event-manager";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import {
|
||||
editorController,
|
||||
editorState
|
||||
} from "../../../screens/editor/tiptap/utils";
|
||||
import { FlatList } from "react-native-actions-sheet";
|
||||
|
||||
type TableOfContentsItem = {
|
||||
level: number;
|
||||
title: string;
|
||||
id: string;
|
||||
top: number;
|
||||
isFocused?: boolean;
|
||||
};
|
||||
|
||||
interface TableOfContentsProps {
|
||||
toc: TableOfContentsItem[];
|
||||
close?: (ctx?: string | undefined) => void;
|
||||
}
|
||||
|
||||
const TableOfContentsItem: React.FC<{
|
||||
item: TableOfContentsItem;
|
||||
close?: (ctx?: string | undefined) => void;
|
||||
}> = ({ item, close }) => {
|
||||
const { colors } = useThemeColors();
|
||||
|
||||
return (
|
||||
<PressableButton
|
||||
customStyle={{
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
flexDirection: "row",
|
||||
paddingLeft: item.level * 12,
|
||||
height: 45
|
||||
}}
|
||||
type={item.isFocused ? "selected" : "transparent"}
|
||||
onPress={() => {
|
||||
editorController.current.commands.scrollIntoViewById(item.id);
|
||||
close?.();
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-start",
|
||||
gap: 10
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name="checkbox-blank-circle"
|
||||
size={8}
|
||||
allowFontScaling
|
||||
color={colors.primary.icon}
|
||||
/>
|
||||
<Paragraph
|
||||
color={
|
||||
item.isFocused
|
||||
? colors.selected.paragraph
|
||||
: colors.primary.paragraph
|
||||
}
|
||||
size={SIZE.md}
|
||||
>
|
||||
{item?.title || "New note"}
|
||||
</Paragraph>
|
||||
</View>
|
||||
</PressableButton>
|
||||
);
|
||||
};
|
||||
|
||||
const TableOfContents = ({ toc, close }: TableOfContentsProps) => {
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
gap: 12,
|
||||
paddingTop: 12
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Heading size={SIZE.lg}>Table of contents</Heading>
|
||||
</View>
|
||||
|
||||
<FlatList
|
||||
data={toc.map((item, index) => {
|
||||
return {
|
||||
...item,
|
||||
isFocused:
|
||||
(editorState().scrollPosition || 0) > item.top &&
|
||||
(editorState().scrollPosition || 0) < toc[index + 1]?.top
|
||||
};
|
||||
})}
|
||||
renderItem={({ item }) => (
|
||||
<TableOfContentsItem item={item} close={close} />
|
||||
)}
|
||||
keyExtractor={(item) => item.id}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
TableOfContents.present = (toc: TableOfContentsItem[]) => {
|
||||
presentSheet({
|
||||
component: (ref, close, update) => (
|
||||
<TableOfContents toc={toc} close={close} />
|
||||
)
|
||||
});
|
||||
};
|
||||
|
||||
export default TableOfContents;
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
Platform,
|
||||
ScrollView,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
ViewStyle,
|
||||
useWindowDimensions
|
||||
@@ -61,7 +62,6 @@ 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 { VaultDialog } from "../../components/dialogs/vault";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import Heading from "../../components/ui/typography/heading";
|
||||
import Seperator from "../../components/ui/seperator";
|
||||
@@ -70,6 +70,10 @@ 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%",
|
||||
|
||||
@@ -310,6 +310,20 @@ const image = toBlobURL("${image.dataurl}", "${image.hash}");
|
||||
keyboardShown = async (keyboardShown: boolean) => {
|
||||
return this.doAsync(`globalThis['keyboardShown']=${keyboardShown};`);
|
||||
};
|
||||
|
||||
getTableOfContents = async () => {
|
||||
const tabId = useTabStore.getState().currentTab;
|
||||
return this.doAsync(`
|
||||
response = editorControllers[${tabId}]?.getTableOfContents() || [];
|
||||
`);
|
||||
};
|
||||
|
||||
scrollIntoViewById = async (id: string) => {
|
||||
const tabId = useTabStore.getState().currentTab;
|
||||
return this.doAsync(`
|
||||
response = editorControllers[${tabId}]?.scrollIntoView("${id}") || [];
|
||||
`);
|
||||
};
|
||||
//todo add replace image function
|
||||
}
|
||||
|
||||
|
||||
@@ -39,5 +39,6 @@ export const EventTypes = {
|
||||
copyToClipboard: "editor-events:copy-to-clipboard",
|
||||
tabsChanged: "editor-events:tabs-changed",
|
||||
showTabs: "editor-events:show-tabs",
|
||||
tabFocused: "editor-events:tab-focused"
|
||||
tabFocused: "editor-events:tab-focused",
|
||||
toc: "editor-events:toc"
|
||||
};
|
||||
|
||||
@@ -35,6 +35,7 @@ export type EditorState = {
|
||||
ready: boolean;
|
||||
saveCount: 0;
|
||||
isAwaitingResult: boolean;
|
||||
scrollPosition: number;
|
||||
};
|
||||
|
||||
export type Settings = {
|
||||
|
||||
@@ -68,6 +68,7 @@ import { EventTypes } from "./editor-events";
|
||||
import { EditorMessage, EditorProps, useEditorType } from "./types";
|
||||
import { useTabStore } from "./use-tab-store";
|
||||
import { EditorEvents, editorState } from "./utils";
|
||||
import TableOfContents from "../../../components/sheets/toc";
|
||||
|
||||
const publishNote = async () => {
|
||||
const user = useUserStore.getState().user;
|
||||
@@ -445,6 +446,9 @@ export const useEditorEvents = (
|
||||
case EventTypes.properties:
|
||||
showActionsheet();
|
||||
break;
|
||||
case EventTypes.scroll:
|
||||
editorState().scrollPosition = editorMessage.value;
|
||||
break;
|
||||
case EventTypes.fullscreen:
|
||||
editorState().isFullscreen = true;
|
||||
eSendEvent(eOpenFullscreenEditor);
|
||||
@@ -476,6 +480,9 @@ export const useEditorEvents = (
|
||||
// console.log("Tabs updated");
|
||||
break;
|
||||
}
|
||||
case EventTypes.toc:
|
||||
TableOfContents.present(editorMessage.value);
|
||||
break;
|
||||
case EventTypes.showTabs: {
|
||||
EditorTabs.present();
|
||||
break;
|
||||
|
||||
838
apps/mobile/package-lock.json
generated
838
apps/mobile/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
45
packages/editor-mobile/package-lock.json
generated
45
packages/editor-mobile/package-lock.json
generated
@@ -14,6 +14,7 @@
|
||||
"@mdi/react": "^1.6.0",
|
||||
"@notesnook/editor": "file:../editor",
|
||||
"@notesnook/theme": "file:../theme",
|
||||
"@szhsin/react-menu": "^4.1.0",
|
||||
"buffer": "^6.0.3",
|
||||
"framer-motion": "^10.16.8",
|
||||
"mdi-react": "9.1.0",
|
||||
@@ -4108,6 +4109,19 @@
|
||||
"url": "https://github.com/sponsors/gregberge"
|
||||
}
|
||||
},
|
||||
"node_modules/@szhsin/react-menu": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@szhsin/react-menu/-/react-menu-4.1.0.tgz",
|
||||
"integrity": "sha512-lYYGUxqJxM2b/jD2Cn5a9RVOvHl9VBMX8qOnHZuX1w08cO2jslykpz5P75D7WnqudLnXsJ4k4+tI+q2U8XIFYw==",
|
||||
"dependencies": {
|
||||
"prop-types": "^15.7.2",
|
||||
"react-transition-state": "^2.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.14.0",
|
||||
"react-dom": ">=16.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tootallnate/once": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz",
|
||||
@@ -4360,7 +4374,7 @@
|
||||
"version": "15.7.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz",
|
||||
"integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==",
|
||||
"dev": true
|
||||
"devOptional": true
|
||||
},
|
||||
"node_modules/@types/q": {
|
||||
"version": "1.5.8",
|
||||
@@ -4384,7 +4398,7 @@
|
||||
"version": "18.2.39",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.39.tgz",
|
||||
"integrity": "sha512-Oiw+ppED6IremMInLV4HXGbfbG6GyziY3kqAwJYOR0PNbkYDmLWQA3a95EhdSmamsvbkJN96ZNN+YD+fGjzSBA==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
"@types/scheduler": "*",
|
||||
@@ -4419,7 +4433,7 @@
|
||||
"version": "0.16.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz",
|
||||
"integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==",
|
||||
"dev": true
|
||||
"devOptional": true
|
||||
},
|
||||
"node_modules/@types/semver": {
|
||||
"version": "7.5.6",
|
||||
@@ -9715,7 +9729,7 @@
|
||||
"version": "9.0.21",
|
||||
"resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz",
|
||||
"integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/immer"
|
||||
@@ -15727,6 +15741,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-transition-state": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/react-transition-state/-/react-transition-state-2.1.1.tgz",
|
||||
"integrity": "sha512-kQx5g1FVu9knoz1T1WkapjUgFz08qQ/g1OmuWGi3/AoEFfS0kStxrPlZx81urjCXdz2d+1DqLpU6TyLW/Ro04Q==",
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/read-cache": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||
@@ -17776,6 +17799,20 @@
|
||||
"is-typedarray": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "4.9.5",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
|
||||
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/unbox-primitive": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz",
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"@mdi/react": "^1.6.0",
|
||||
"@notesnook/editor": "file:../editor",
|
||||
"@notesnook/theme": "file:../theme",
|
||||
"@szhsin/react-menu": "^4.1.0",
|
||||
"buffer": "^6.0.3",
|
||||
"framer-motion": "^10.16.8",
|
||||
"mdi-react": "9.1.0",
|
||||
|
||||
@@ -20,6 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import {
|
||||
Editor,
|
||||
getFontById,
|
||||
getTableOfContents,
|
||||
PortalProvider,
|
||||
Toolbar,
|
||||
usePermissionHandler,
|
||||
@@ -49,7 +50,6 @@ import Header from "./header";
|
||||
import StatusBar from "./statusbar";
|
||||
import Tags from "./tags";
|
||||
import Title from "./title";
|
||||
import FingerprintIcon from "mdi-react/FingerprintIcon";
|
||||
|
||||
globalThis.toBlobURL = toBlobURL;
|
||||
|
||||
@@ -160,7 +160,14 @@ const Tiptap = ({ settings }: { settings: Settings }) => {
|
||||
});
|
||||
}, []);
|
||||
|
||||
const controller = useEditorController(update);
|
||||
const controller = useEditorController({
|
||||
update,
|
||||
getTableOfContents: () => {
|
||||
return !containerRef.current
|
||||
? []
|
||||
: getTableOfContents(containerRef.current);
|
||||
}
|
||||
});
|
||||
const controllerRef = useRef(controller);
|
||||
|
||||
globalThis.editorControllers[tab.id] = controller;
|
||||
|
||||
@@ -17,32 +17,73 @@ 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 {
|
||||
ControlledMenu,
|
||||
// applyStatics
|
||||
MenuItem as MenuItemInner,
|
||||
SubMenu as SubMenuInner,
|
||||
MenuDivider
|
||||
} from "@szhsin/react-menu";
|
||||
import ArrowBackIcon from "mdi-react/ArrowBackIcon";
|
||||
import ArrowULeftTopIcon from "mdi-react/ArrowULeftTopIcon";
|
||||
import ArrowURightTopIcon from "mdi-react/ArrowURightTopIcon";
|
||||
import CrownIcon from "mdi-react/CrownIcon";
|
||||
import DotsHorizontalIcon from "mdi-react/DotsHorizontalIcon";
|
||||
import DotsVerticalIcon from "mdi-react/DotsVerticalIcon";
|
||||
import FullscreenIcon from "mdi-react/FullscreenIcon";
|
||||
import MagnifyIcon from "mdi-react/MagnifyIcon";
|
||||
import React from "react";
|
||||
import React, { useRef, useState } from "react";
|
||||
import { useSafeArea } from "../hooks/useSafeArea";
|
||||
import { useTabContext, useTabStore } from "../hooks/useTabStore";
|
||||
import { EventTypes, Settings } from "../utils";
|
||||
import styles from "./styles.module.css";
|
||||
import TableOfContentsIcon from "mdi-react/TableOfContentsIcon";
|
||||
const menuClassName = ({ state }: any) =>
|
||||
state === "opening"
|
||||
? styles.menuOpening
|
||||
: state === "closing"
|
||||
? styles.menuClosing
|
||||
: styles.menu;
|
||||
|
||||
const menuItemClassName = ({ hover, disabled }: any) =>
|
||||
disabled
|
||||
? styles.menuItemDisabled
|
||||
: hover
|
||||
? styles.menuItemHover
|
||||
: styles.menuItem;
|
||||
|
||||
const submenuItemClassName = (modifiers: any) =>
|
||||
`${styles.submenuItem} ${menuItemClassName(modifiers)}`;
|
||||
|
||||
const MenuItem = (props: any) => (
|
||||
<MenuItemInner {...props} className={menuItemClassName} />
|
||||
);
|
||||
|
||||
const SubMenu = (props: any) => (
|
||||
<SubMenuInner
|
||||
{...props}
|
||||
menuClassName={menuClassName}
|
||||
itemProps={{ className: submenuItemClassName }}
|
||||
offsetY={-7}
|
||||
/>
|
||||
);
|
||||
|
||||
const Button = ({
|
||||
onPress,
|
||||
children,
|
||||
style,
|
||||
preventDefault = true
|
||||
preventDefault = true,
|
||||
fwdRef
|
||||
}: {
|
||||
onPress: () => void;
|
||||
children: React.ReactNode;
|
||||
style: React.CSSProperties;
|
||||
preventDefault?: boolean;
|
||||
fwdRef?: any;
|
||||
}) => {
|
||||
return (
|
||||
<button
|
||||
ref={fwdRef}
|
||||
className={styles.btn_header}
|
||||
style={style}
|
||||
onMouseDown={(e) => {
|
||||
@@ -70,6 +111,8 @@ function Header({
|
||||
const editor = editors[tab.id];
|
||||
const insets = useSafeArea();
|
||||
const openedTabsCount = useTabStore((state) => state.tabs.length);
|
||||
const [isOpen, setOpen] = useState(false);
|
||||
const btnRef = useRef(null);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -79,7 +122,8 @@ function Header({
|
||||
height: noHeader ? `${insets.top}px` : `${50 + insets.top}px`,
|
||||
backgroundColor: "var(--nn_primary_background)",
|
||||
position: "sticky",
|
||||
width: "100vw"
|
||||
width: "100vw",
|
||||
zIndex: 99999
|
||||
}}
|
||||
>
|
||||
{noHeader ? null : (
|
||||
@@ -318,8 +362,9 @@ function Header({
|
||||
|
||||
<Button
|
||||
onPress={() => {
|
||||
post(EventTypes.properties, undefined, tab.id, tab.noteId);
|
||||
setOpen(!isOpen);
|
||||
}}
|
||||
fwdRef={btnRef}
|
||||
preventDefault={false}
|
||||
style={{
|
||||
borderWidth: 0,
|
||||
@@ -334,7 +379,7 @@ function Header({
|
||||
position: "relative"
|
||||
}}
|
||||
>
|
||||
<DotsHorizontalIcon
|
||||
<DotsVerticalIcon
|
||||
size={25 * settings.fontScale}
|
||||
style={{
|
||||
position: "absolute"
|
||||
@@ -342,6 +387,63 @@ function Header({
|
||||
color="var(--nn_primary_paragraph)"
|
||||
/>
|
||||
</Button>
|
||||
|
||||
<ControlledMenu
|
||||
align="end"
|
||||
anchorRef={btnRef}
|
||||
transition
|
||||
state={isOpen ? "open" : "closed"}
|
||||
menuClassName={menuClassName}
|
||||
onClose={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
autoFocus={false}
|
||||
onItemClick={(e) => {
|
||||
switch (e.value) {
|
||||
case "toc":
|
||||
post(
|
||||
EventTypes.toc,
|
||||
editorControllers[tab.id]?.getTableOfContents(),
|
||||
tab.id,
|
||||
tab.noteId
|
||||
);
|
||||
break;
|
||||
case "properties":
|
||||
logger("info", "post properties...");
|
||||
post(EventTypes.properties, undefined, tab.id, tab.noteId);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MenuItem
|
||||
value="toc"
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: 10
|
||||
}}
|
||||
>
|
||||
<TableOfContentsIcon
|
||||
size={22 * settings.fontScale}
|
||||
color="var(--nn_primary_paragraph)"
|
||||
/>
|
||||
Table of contents
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
value="properties"
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: 10
|
||||
}}
|
||||
>
|
||||
<DotsHorizontalIcon
|
||||
size={22 * settings.fontScale}
|
||||
color="var(--nn_primary_paragraph)"
|
||||
/>
|
||||
Note Properties
|
||||
</MenuItem>
|
||||
</ControlledMenu>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -53,6 +53,7 @@ function StatusBar({ container }: { container: RefObject<HTMLDivElement> }) {
|
||||
|
||||
const onScroll = React.useCallback((event: Event) => {
|
||||
const currentOffset = (event.target as HTMLElement)?.scrollTop;
|
||||
post("editor-event:scroll", currentOffset);
|
||||
if (currentOffset < 200) {
|
||||
if (stickyRef.current) {
|
||||
stickyRef.current = false;
|
||||
|
||||
@@ -31,3 +31,76 @@
|
||||
.container::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
@keyframes menuShow {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes menuHide {
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.menu {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
box-sizing: border-box;
|
||||
z-index: 999;
|
||||
list-style: none;
|
||||
user-select: none;
|
||||
padding: 6px;
|
||||
font-family: sans-serif;
|
||||
font-size: 0.95em;
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
box-shadow: 1px 1px 20px 1px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 6px;
|
||||
background-color: var(--nn_secondary_background);
|
||||
min-width: 12rem;
|
||||
}
|
||||
|
||||
.menuOpening {
|
||||
composes: menu;
|
||||
animation: menuShow 0.15s ease-out;
|
||||
}
|
||||
|
||||
.menuClosing {
|
||||
composes: menu;
|
||||
animation: menuHide 0.2s ease-out forwards;
|
||||
}
|
||||
|
||||
.menuItem {
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
padding: 0.375rem 0.625rem;
|
||||
color: var(--nn_primary_paragraph);
|
||||
font-family: 'Open Sans';
|
||||
padding: 12px 6px;
|
||||
|
||||
}
|
||||
|
||||
.menuItemHover {
|
||||
composes: menuItem;
|
||||
color: #fff;
|
||||
background-color: var(--nn_primary_hover);
|
||||
}
|
||||
|
||||
.menuItemDisabled {
|
||||
composes: menuItem;
|
||||
cursor: default;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.menu:focus,
|
||||
.menuItem:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.menuDivider {
|
||||
height: 1px;
|
||||
margin: 0.1rem 0.1rem;
|
||||
background-color: var(--nn_primary_border);
|
||||
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ 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 { Editor } from "@notesnook/editor";
|
||||
import { Editor, scrollIntoViewById } from "@notesnook/editor";
|
||||
import {
|
||||
ThemeDefinition,
|
||||
useThemeColors,
|
||||
@@ -106,9 +106,17 @@ export type EditorController = {
|
||||
updateTab: () => void;
|
||||
loading: boolean;
|
||||
setLoading: (value: boolean) => void;
|
||||
getTableOfContents: () => any[];
|
||||
scrollIntoView: (id: string) => void;
|
||||
};
|
||||
|
||||
export function useEditorController(update: () => void): EditorController {
|
||||
export function useEditorController({
|
||||
update,
|
||||
getTableOfContents
|
||||
}: {
|
||||
update: () => void;
|
||||
getTableOfContents: () => any[];
|
||||
}): EditorController {
|
||||
const tab = useTabContext();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const setTheme = useThemeEngineStore((store) => store.setTheme);
|
||||
@@ -317,6 +325,8 @@ export function useEditorController(update: () => void): EditorController {
|
||||
};
|
||||
|
||||
return {
|
||||
getTableOfContents: getTableOfContents,
|
||||
scrollIntoView: (id: string) => scrollIntoViewById(id),
|
||||
contentChange,
|
||||
selectionChange,
|
||||
titleChange,
|
||||
|
||||
@@ -173,7 +173,8 @@ export const EventTypes = {
|
||||
copyToClipboard: "editor-events:copy-to-clipboard",
|
||||
tabsChanged: "editor-events:tabs-changed",
|
||||
showTabs: "editor-events:show-tabs",
|
||||
tabFocused: "editor-events:tab-focused"
|
||||
tabFocused: "editor-events:tab-focused",
|
||||
toc: "editor-events:toc"
|
||||
} as const;
|
||||
|
||||
export function isReactNative(): boolean {
|
||||
|
||||
77
packages/editor/src/extensions/block-id/blockid.ts
Normal file
77
packages/editor/src/extensions/block-id/blockid.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { Node } from "@tiptap/core";
|
||||
import { Plugin } from "@tiptap/pm/state";
|
||||
import { nanoid } from "nanoid";
|
||||
import { getChangedNodes } from "../../utils/prosemirror";
|
||||
|
||||
const types: { [name: string]: boolean } = {
|
||||
heading: true,
|
||||
paragraph: true
|
||||
};
|
||||
|
||||
export const BlockId = Node.create({
|
||||
name: "blockId",
|
||||
|
||||
addGlobalAttributes() {
|
||||
return [
|
||||
{
|
||||
types: Object.keys(types),
|
||||
attributes: {
|
||||
blockId: {
|
||||
default: null,
|
||||
keepOnSplit: false,
|
||||
parseHTML: (element) => {
|
||||
const id = element.getAttribute("data-block-id");
|
||||
return id || null;
|
||||
},
|
||||
renderHTML: (attributes) => {
|
||||
return {
|
||||
"data-block-id": attributes?.blockId
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
},
|
||||
addProseMirrorPlugins() {
|
||||
return [
|
||||
new Plugin({
|
||||
appendTransaction: (_transactions, oldState, newState) => {
|
||||
// no changes
|
||||
if (newState.doc === oldState.doc) {
|
||||
return;
|
||||
}
|
||||
const tr = newState.tr;
|
||||
|
||||
const blockIds = new Set<string>();
|
||||
const blocksWithoutBlockId: any[] = [];
|
||||
|
||||
for (const tr of _transactions) {
|
||||
blocksWithoutBlockId.push(
|
||||
...getChangedNodes(tr, {
|
||||
descend: false,
|
||||
predicate: (n) => {
|
||||
const shouldInclude =
|
||||
n.isBlock &&
|
||||
(!n.attrs.blockId || blockIds.has(n.attrs.blockId));
|
||||
|
||||
if (n.attrs.blockId) blockIds.add(n.attrs.blockId);
|
||||
return shouldInclude;
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
for (const { node, pos } of blocksWithoutBlockId) {
|
||||
const id = nanoid(8);
|
||||
tr.setNodeMarkup(pos, undefined, {
|
||||
...node.attrs,
|
||||
blockId: id
|
||||
});
|
||||
}
|
||||
return tr;
|
||||
}
|
||||
})
|
||||
];
|
||||
}
|
||||
});
|
||||
5
packages/editor/src/extensions/block-id/index.ts
Normal file
5
packages/editor/src/extensions/block-id/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { BlockId } from "./blockid";
|
||||
|
||||
export * from "./blockid";
|
||||
|
||||
export default BlockId;
|
||||
@@ -79,6 +79,7 @@ import Clipboard, { ClipboardOptions } from "./extensions/clipboard";
|
||||
import Blockquote from "./extensions/blockquote";
|
||||
import { Quirks } from "./extensions/quirks";
|
||||
import { LIST_NODE_TYPES } from "./utils/node-types";
|
||||
import BlockId from "./extensions/block-id";
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line no-var
|
||||
@@ -210,6 +211,7 @@ const useTiptap = (
|
||||
];
|
||||
}
|
||||
}),
|
||||
BlockId,
|
||||
Blockquote,
|
||||
CharacterCount,
|
||||
Underline,
|
||||
@@ -347,6 +349,7 @@ export * from "./toolbar";
|
||||
export * from "./types";
|
||||
export * from "./utils/word-counter";
|
||||
export * from "./utils/font";
|
||||
export * from "./utils/toc";
|
||||
export {
|
||||
useTiptap,
|
||||
Toolbar,
|
||||
|
||||
35
packages/editor/src/utils/toc.ts
Normal file
35
packages/editor/src/utils/toc.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
export function getTableOfContents(content: HTMLElement) {
|
||||
const headings = content.querySelectorAll("h1, h2, h3, h4, h5, h6");
|
||||
const tableOfContents: {
|
||||
level: number;
|
||||
title: string | null;
|
||||
id: string | null;
|
||||
top: number;
|
||||
}[] = [];
|
||||
|
||||
for (let i = 0; i < headings.length; i++) {
|
||||
const heading = headings[i];
|
||||
const level = parseInt(heading.tagName[1]);
|
||||
const title = heading.textContent;
|
||||
const id = heading.getAttribute("data-block-id");
|
||||
tableOfContents.push({
|
||||
level,
|
||||
title,
|
||||
id,
|
||||
top: (heading as HTMLElement).offsetTop
|
||||
});
|
||||
}
|
||||
|
||||
return tableOfContents;
|
||||
}
|
||||
|
||||
export function scrollIntoViewById(id: string) {
|
||||
const element = document.querySelector(`[data-block-id="${id}"]`);
|
||||
if (element) {
|
||||
element.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "center",
|
||||
inline: "nearest"
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user