Compare commits

..

3 Commits

Author SHA1 Message Date
Philipinho a308e085b5 update y-prosemirror 2025-11-27 20:38:34 +00:00
Philipinho 73b6e95197 update tiptap 2025-11-27 20:32:44 +00:00
Philipinho b79b693f50 * wip
* fix styling
* use nanoid
2025-11-27 20:29:21 +00:00
8 changed files with 501 additions and 447 deletions
@@ -380,6 +380,7 @@
"Real-time editor connection lost. Retrying...": "Real-time editor connection lost. Retrying...",
"Table of contents": "Table of contents",
"Add headings (H1, H2, H3) to generate a table of contents.": "Add headings (H1, H2, H3) to generate a table of contents.",
"No table of contents yet": "No table of contents yet",
"Share": "Share",
"Public sharing": "Public sharing",
"Shared by": "Shared by",
@@ -1,13 +1,18 @@
.header {
}
.container {
counter-reset: h1 h2 h3 h4;
border-left: 1px solid
light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
}
.emptyState {
color: light-dark(
var(--mantine-color-dark-3),
var(--mantine-color-dark-2)
) !important;
}
.link {
text-decoration: none;
outline: none;
cursor: pointer;
display: block;
@@ -19,9 +24,41 @@
font-size: var(--mantine-font-size-sm);
line-height: var(--mantine-line-height-sm);
padding: 6px;
border-top-right-radius: var(--mantine-radius-sm);
border-bottom-right-radius: var(--mantine-radius-sm);
border: none;
border-bottom: none !important;
padding-left: calc(var(--level) * 1rem);
&[style*="--level: 1"] {
counter-increment: h1;
counter-reset: h2 h3 h4;
padding-left: 6px;
&::before {
content: counter(h1) ". ";
}
}
&[style*="--level: 2"] {
counter-increment: h2;
counter-reset: h3 h4;
&::before {
content: counter(h2) ". ";
}
}
&[style*="--level: 3"] {
counter-increment: h3;
counter-reset: h4;
&::before {
content: counter(h3) ". ";
}
}
&[style*="--level: 4"] {
counter-increment: h4;
&::before {
content: counter(h4) ". ";
}
}
@mixin hover {
background-color: light-dark(
@@ -29,33 +66,4 @@
var(--mantine-color-dark-6)
);
}
@media (max-width: $mantine-breakpoint-sm) {
& {
border: none !important;
padding-left: 0px;
}
}
}
.linkActive {
font-weight: 500;
border-left-color: light-dark(
var(--mantine-color-grey-5),
var(--mantine-color-grey-3)
);
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1));
&,
&:hover {
background-color: light-dark(
var(--mantine-color-gray-3),
var(--mantine-color-dark-5)
) !important;
}
}
.leftBorder {
border-left: 1px solid
light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
}
@@ -2,48 +2,74 @@ import { Editor as CoreEditor } from "@tiptap/core";
import { TableOfContentsStorage } from "@tiptap/extension-table-of-contents";
import { NodeViewWrapper, useEditorState } from "@tiptap/react";
import { memo } from "react";
import { clsx } from "clsx";
import classes from "./table-of-contents-nodeview.module.css";
import { useTranslation } from "react-i18next";
import { TextSelection } from "@tiptap/pm/state";
export type TableOfContentsProps = {
editor: CoreEditor;
onItemClick?: () => void;
};
export const TableOfContentsNodeview = memo(
({ editor, onItemClick }: TableOfContentsProps) => {
({ editor }: TableOfContentsProps) => {
const content = useEditorState({
editor,
selector: (ctx) =>
(ctx.editor.storage.tableOfContents as TableOfContentsStorage)?.content,
});
const { t } = useTranslation();
const onTocItemClick = (e, id) => {
e.preventDefault();
if (editor) {
const element = editor.view.dom.querySelector(`[data-toc-id="${id}"`);
const pos = editor.view.posAtDOM(element, 0);
// set focus
const tr = editor.view.state.tr;
tr.setSelection(new TextSelection(tr.doc.resolve(pos)));
editor.view.dispatch(tr);
editor.view.focus();
if (history.pushState) {
history.pushState(null, null, `#${id}`);
}
window.scrollTo({
top: element.getBoundingClientRect().top + window.scrollY,
behavior: "smooth",
});
}
};
return (
<NodeViewWrapper>
<div contentEditable={false}>
<div className={classes.header}>Table of contents</div>
{content.length > 0 ? (
<div className={classes.container}>
{content
.filter((item) => item.level <= 3)
.filter((item) => item.level <= 4)
.map((item) => (
<a
key={item.id}
href={`#${item.id}`}
style={{ marginLeft: `${1 * item.level - 1}rem` }}
onClick={onItemClick}
className={clsx(
classes.link,
item.isActive && classes.linkActive,
)}
style={{ "--level": item.level } as React.CSSProperties}
onClick={(e) => onTocItemClick(e, item.id)}
className={classes.link}
data-item-index={item.itemIndex}
draggable="false"
>
{item.itemIndex}. {item.textContent}
{item.textContent}
</a>
))}
</div>
) : (
<div className={classes.emptyState}>
Start adding headlines to your document
{t("No table of contents yet")}
</div>
)}
</div>
@@ -42,6 +42,7 @@ import {
Subpages,
TableDndExtension,
TableOfContentsNode,
generateNodeId,
} from "@docmost/editor-ext";
import {
randomElement,
@@ -244,7 +245,9 @@ export const mainExtensions = [
};
},
}).configure(),
TiptapTableOfContents,
TiptapTableOfContents.configure({
getId: () => generateNodeId(),
}),
TableOfContentsNode.configure({
view: TableOfContentsNodeview,
}),
@@ -34,7 +34,9 @@ import {
Mention,
Subpages,
TableOfContentsNode,
generateNodeId,
} from '@docmost/editor-ext';
import { TableOfContents as TiptapTableOfContents } from '@tiptap/extension-table-of-contents';
import { generateText, getSchema, JSONContent } from '@tiptap/core';
import { generateHTML, generateJSON } from '../common/helpers/prosemirror/html';
// @tiptap/html library works best for generating prosemirror json state but not HTML
@@ -82,6 +84,9 @@ export const tiptapExtensions = [
Mention,
Subpages,
TableOfContentsNode,
TiptapTableOfContents.configure({
getId: () => generateNodeId(),
}),
] as any;
export function jsonToHtml(tiptapJson: any) {
+41 -39
View File
@@ -21,48 +21,48 @@
"@braintree/sanitize-url": "^7.1.0",
"@docmost/editor-ext": "workspace:*",
"@floating-ui/dom": "^1.7.3",
"@hocuspocus/extension-redis": "^2.15.2",
"@hocuspocus/provider": "^2.15.2",
"@hocuspocus/server": "^2.15.2",
"@hocuspocus/transformer": "^2.15.2",
"@hocuspocus/extension-redis": "^2.15.3",
"@hocuspocus/provider": "^2.15.3",
"@hocuspocus/server": "^2.15.3",
"@hocuspocus/transformer": "^2.15.3",
"@joplin/turndown": "^4.0.74",
"@joplin/turndown-plugin-gfm": "^1.0.56",
"@sindresorhus/slugify": "1.1.0",
"@tiptap/core": "^2.10.3",
"@tiptap/extension-code-block": "^2.10.3",
"@tiptap/extension-code-block-lowlight": "^2.10.3",
"@tiptap/extension-collaboration": "^2.10.3",
"@tiptap/extension-collaboration-cursor": "^2.10.3",
"@tiptap/extension-color": "^2.10.3",
"@tiptap/extension-document": "^2.10.3",
"@tiptap/extension-heading": "^2.10.3",
"@tiptap/extension-highlight": "^2.10.3",
"@tiptap/extension-history": "^2.10.3",
"@tiptap/extension-image": "^2.10.3",
"@tiptap/extension-link": "^2.10.3",
"@tiptap/extension-list-item": "^2.10.3",
"@tiptap/extension-list-keymap": "^2.10.3",
"@tiptap/extension-placeholder": "^2.10.3",
"@tiptap/extension-subscript": "^2.10.3",
"@tiptap/extension-superscript": "^2.10.3",
"@tiptap/extension-table": "^2.10.3",
"@tiptap/extension-table-cell": "^2.10.3",
"@tiptap/extension-table-header": "^2.10.3",
"@tiptap/core": "^2.27.1",
"@tiptap/extension-code-block": "^2.27.1",
"@tiptap/extension-code-block-lowlight": "^2.27.1",
"@tiptap/extension-collaboration": "^2.27.1",
"@tiptap/extension-collaboration-cursor": "^2.27.1",
"@tiptap/extension-color": "^2.27.1",
"@tiptap/extension-document": "^2.27.1",
"@tiptap/extension-heading": "^2.27.1",
"@tiptap/extension-highlight": "^2.27.1",
"@tiptap/extension-history": "^2.27.1",
"@tiptap/extension-image": "^2.27.1",
"@tiptap/extension-link": "^2.27.1",
"@tiptap/extension-list-item": "^2.27.1",
"@tiptap/extension-list-keymap": "^2.27.1",
"@tiptap/extension-placeholder": "^2.27.1",
"@tiptap/extension-subscript": "^2.27.1",
"@tiptap/extension-superscript": "^2.27.1",
"@tiptap/extension-table": "^2.27.1",
"@tiptap/extension-table-cell": "^2.27.1",
"@tiptap/extension-table-header": "^2.27.1",
"@tiptap/extension-table-of-contents": "2.26.3",
"@tiptap/extension-table-row": "^2.10.3",
"@tiptap/extension-task-item": "^2.10.3",
"@tiptap/extension-task-list": "^2.10.3",
"@tiptap/extension-text": "^2.10.3",
"@tiptap/extension-text-align": "^2.10.3",
"@tiptap/extension-text-style": "^2.10.3",
"@tiptap/extension-typography": "^2.10.3",
"@tiptap/extension-underline": "^2.10.3",
"@tiptap/extension-youtube": "^2.10.3",
"@tiptap/html": "^2.10.3",
"@tiptap/pm": "^2.10.3",
"@tiptap/react": "^2.10.3",
"@tiptap/starter-kit": "^2.10.3",
"@tiptap/suggestion": "^2.10.3",
"@tiptap/extension-table-row": "^2.27.1",
"@tiptap/extension-task-item": "^2.27.1",
"@tiptap/extension-task-list": "^2.27.1",
"@tiptap/extension-text": "^2.27.1",
"@tiptap/extension-text-align": "^2.27.1",
"@tiptap/extension-text-style": "^2.27.1",
"@tiptap/extension-typography": "^2.27.1",
"@tiptap/extension-underline": "^2.27.1",
"@tiptap/extension-youtube": "^2.27.1",
"@tiptap/html": "^2.27.1",
"@tiptap/pm": "^2.27.1",
"@tiptap/react": "^2.27.1",
"@tiptap/starter-kit": "^2.27.1",
"@tiptap/suggestion": "^2.27.1",
"@types/qrcode": "^1.5.5",
"bytes": "^3.1.2",
"cross-env": "^7.0.3",
@@ -77,6 +77,7 @@
"qrcode": "^1.5.4",
"uuid": "^11.1.0",
"y-indexeddb": "^9.0.12",
"y-prosemirror": "1.3.7",
"yjs": "^13.6.27"
},
"devDependencies": {
@@ -99,7 +100,8 @@
"react-arborist@3.4.0": "patches/react-arborist@3.4.0.patch"
},
"overrides": {
"jsdom": "25.0.1"
"jsdom": "25.0.1",
"y-prosemirror": "1.3.7"
},
"neverBuiltDependencies": []
}
+6 -2
View File
@@ -5,6 +5,7 @@ import { CellSelection, TableMap } from "@tiptap/pm/tables";
import { Node, ResolvedPos } from "@tiptap/pm/model";
import Table from "@tiptap/extension-table";
import { sanitizeUrl as braintreeSanitizeUrl } from "@braintree/sanitize-url";
import { customAlphabet } from "nanoid";
export const isRectSelected = (rect: any) => (selection: CellSelection) => {
const map = TableMap.get(selection.$anchorCell.node(-1));
@@ -383,9 +384,12 @@ export function icon(name: string) {
export function sanitizeUrl(url: string | undefined): string {
if (!url) return "";
const sanitized = braintreeSanitizeUrl(url);
// Return empty string instead of "about:blank"
return sanitized === "about:blank" ? "" : sanitized;
}
const alphabet = "abcdefghijklmnopqrstuvwxyz";
export const generateNodeId = customAlphabet(alphabet, 15);
+363 -358
View File
File diff suppressed because it is too large Load Diff