mirror of
https://github.com/docmost/docmost.git
synced 2026-05-07 14:43:06 +08:00
* wip
* fix styling * use nanoid
This commit is contained in:
@@ -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",
|
||||
|
||||
+42
-34
@@ -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));
|
||||
}
|
||||
|
||||
+39
-13
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user