(null);
+
+ const handleScrollToHeading = (position: number) => {
+ const { view } = props.editor;
+
+ const headerOffset = parseInt(
+ window.getComputedStyle(headerPaddingRef.current).getPropertyValue("top"),
+ );
+
+ const { node } = view.domAtPos(position);
+ const element = node as HTMLElement;
+ const scrollPosition =
+ element.getBoundingClientRect().top + window.scrollY - headerOffset;
+
+ window.scrollTo({
+ top: scrollPosition,
+ behavior: "smooth",
+ });
+
+ const tr = view.state.tr;
+ tr.setSelection(new TextSelection(tr.doc.resolve(position)));
+ view.dispatch(tr);
+ view.focus();
+ };
+
+ const handleUpdate = () => {
+ const result = recalculateLinks(props.editor?.$nodes("heading"));
+ setLinks(result.links);
+ setHeadingDOMNodes(result.nodes);
+ };
+
+ useEffect(() => {
+ props.editor?.on("update", handleUpdate);
+
+ return () => {
+ props.editor?.off("update", handleUpdate);
+ };
+ }, [props.editor]);
+
+ useEffect(() => {
+ handleUpdate();
+ }, []);
+
+ useEffect(() => {
+ try {
+ const observeHandler = (entries: IntersectionObserverEntry[]) => {
+ entries.forEach((entry) => {
+ if (entry.isIntersecting) {
+ setActiveElement(entry.target as HTMLElement);
+ }
+ });
+ };
+
+ let headerOffset = 0;
+ if (headerPaddingRef.current) {
+ headerOffset = parseInt(
+ window
+ .getComputedStyle(headerPaddingRef.current)
+ .getPropertyValue("top"),
+ );
+ }
+ const observerOptions: IntersectionObserverInit = {
+ rootMargin: `-${headerOffset}px 0px -85% 0px`,
+ threshold: 0,
+ root: null,
+ };
+ const observer = new IntersectionObserver(
+ observeHandler,
+ observerOptions,
+ );
+
+ headingDOMNodes.forEach((heading) => {
+ observer.observe(heading);
+ });
+ return () => {
+ headingDOMNodes.forEach((heading) => {
+ observer.unobserve(heading);
+ });
+ };
+ } catch (err) {
+ console.log(err);
+ }
+ }, [headingDOMNodes, props.editor]);
+
+ if (!links.length) {
+ return (
+ <>
+
+ {t("Add headings (H1, H2, H3) to generate a table of contents.")}
+
+ >
+ );
+ }
+
+ return (
+ <>
+
+ {links.map((item, idx) => (
+
+ component="button"
+ onClick={() => handleScrollToHeading(item.position)}
+ key={idx}
+ className={clsx(classes.link, {
+ [classes.linkActive]: item.element === activeElement,
+ })}
+ style={{
+ paddingLeft: `calc(${item.level} * var(--mantine-spacing-md))`,
+ }}
+ >
+ {item.label}
+
+ ))}
+
+
+ >
+ );
+};
diff --git a/apps/client/src/features/editor/extensions/extensions.ts b/apps/client/src/features/editor/extensions/extensions.ts
index ecdac0c1..c131ad70 100644
--- a/apps/client/src/features/editor/extensions/extensions.ts
+++ b/apps/client/src/features/editor/extensions/extensions.ts
@@ -228,4 +228,4 @@ export const collabExtensions: CollabExtensions = (provider, user) => [
color: randomElement(userColors),
},
}),
-];
+];
\ No newline at end of file
diff --git a/apps/client/src/features/page/components/header/page-header-menu.tsx b/apps/client/src/features/page/components/header/page-header-menu.tsx
index 4883b52b..fefd3e28 100644
--- a/apps/client/src/features/page/components/header/page-header-menu.tsx
+++ b/apps/client/src/features/page/components/header/page-header-menu.tsx
@@ -6,6 +6,7 @@ import {
IconFileExport,
IconHistory,
IconLink,
+ IconList,
IconMessage,
IconPrinter,
IconTrash,
@@ -56,7 +57,7 @@ export default function PageHeaderMenu({ readOnly }: PageHeaderMenuProps) {
)}
-
+
+
+ toggleAside("toc")}
+ >
+
+
+
+
>
);
diff --git a/apps/client/src/features/user/components/page-width-pref.tsx b/apps/client/src/features/user/components/page-width-pref.tsx
index b9a43248..6ad66062 100644
--- a/apps/client/src/features/user/components/page-width-pref.tsx
+++ b/apps/client/src/features/user/components/page-width-pref.tsx
@@ -1,7 +1,7 @@
-import { Group, Text, Switch, MantineSize } from "@mantine/core";
-import { useAtom } from "jotai/index";
import { userAtom } from "@/features/user/atoms/current-user-atom.ts";
import { updateUser } from "@/features/user/services/user-service.ts";
+import { Group, MantineSize, Switch, Text } from "@mantine/core";
+import { useAtom } from "jotai/index";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
@@ -26,6 +26,7 @@ interface PageWidthToggleProps {
size?: MantineSize;
label?: string;
}
+
export function PageWidthToggle({ size, label }: PageWidthToggleProps) {
const { t } = useTranslation();
const [user, setUser] = useAtom(userAtom);
@@ -50,4 +51,4 @@ export function PageWidthToggle({ size, label }: PageWidthToggleProps) {
aria-label={t("Toggle full page width")}
/>
);
-}
+}
\ No newline at end of file
diff --git a/apps/client/src/features/user/types/user.types.ts b/apps/client/src/features/user/types/user.types.ts
index e9bf1220..5439580f 100644
--- a/apps/client/src/features/user/types/user.types.ts
+++ b/apps/client/src/features/user/types/user.types.ts
@@ -30,4 +30,4 @@ export interface IUserSettings {
preferences: {
fullPageWidth: boolean;
};
-}
+}
\ No newline at end of file
diff --git a/apps/client/src/pages/settings/account/account-preferences.tsx b/apps/client/src/pages/settings/account/account-preferences.tsx
index 06d62af7..26daa488 100644
--- a/apps/client/src/pages/settings/account/account-preferences.tsx
+++ b/apps/client/src/pages/settings/account/account-preferences.tsx
@@ -2,8 +2,8 @@ import SettingsTitle from "@/components/settings/settings-title.tsx";
import AccountLanguage from "@/features/user/components/account-language.tsx";
import AccountTheme from "@/features/user/components/account-theme.tsx";
import PageWidthPref from "@/features/user/components/page-width-pref.tsx";
-import { Divider } from "@mantine/core";
import { getAppName } from "@/lib/config.ts";
+import { Divider } from "@mantine/core";
import { Helmet } from "react-helmet-async";
import { useTranslation } from "react-i18next";
diff --git a/apps/server/src/core/user/dto/update-user.dto.ts b/apps/server/src/core/user/dto/update-user.dto.ts
index ff3201c5..cdf085bb 100644
--- a/apps/server/src/core/user/dto/update-user.dto.ts
+++ b/apps/server/src/core/user/dto/update-user.dto.ts
@@ -1,6 +1,6 @@
import { OmitType, PartialType } from '@nestjs/mapped-types';
-import { CreateUserDto } from '../../auth/dto/create-user.dto';
import { IsBoolean, IsOptional, IsString } from 'class-validator';
+import { CreateUserDto } from '../../auth/dto/create-user.dto';
export class UpdateUserDto extends PartialType(
OmitType(CreateUserDto, ['password'] as const),
diff --git a/apps/server/src/core/user/user.service.ts b/apps/server/src/core/user/user.service.ts
index 7909b548..434f4cac 100644
--- a/apps/server/src/core/user/user.service.ts
+++ b/apps/server/src/core/user/user.service.ts
@@ -1,10 +1,10 @@
+import { UserRepo } from '@docmost/db/repos/user/user.repo';
import {
BadRequestException,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { UpdateUserDto } from './dto/update-user.dto';
-import { UserRepo } from '@docmost/db/repos/user/user.repo';
@Injectable()
export class UserService {
@@ -27,8 +27,9 @@ export class UserService {
// preference update
if (typeof updateUserDto.fullPageWidth !== 'undefined') {
- return this.updateUserPageWidthPreference(
+ return this.userRepo.updatePreference(
userId,
+ 'fullPageWidth',
updateUserDto.fullPageWidth,
);
}
@@ -55,12 +56,4 @@ export class UserService {
await this.userRepo.updateUser(updateUserDto, userId, workspaceId);
return user;
}
-
- async updateUserPageWidthPreference(userId: string, fullPageWidth: boolean) {
- return this.userRepo.updatePreference(
- userId,
- 'fullPageWidth',
- fullPageWidth,
- );
- }
}
diff --git a/packages/editor-ext/src/index.ts b/packages/editor-ext/src/index.ts
index 9e211778..e9915e78 100644
--- a/packages/editor-ext/src/index.ts
+++ b/packages/editor-ext/src/index.ts
@@ -16,4 +16,4 @@ export * from "./lib/drawio";
export * from "./lib/excalidraw";
export * from "./lib/embed";
export * from "./lib/mention";
-export * from "./lib/markdown";
+export * from "./lib/markdown";
\ No newline at end of file