diff --git a/app/components/ContextMenu/MenuItem.js b/app/components/ContextMenu/MenuItem.js
index a82d9222c..7ebc4bc9a 100644
--- a/app/components/ContextMenu/MenuItem.js
+++ b/app/components/ContextMenu/MenuItem.js
@@ -15,6 +15,7 @@ type Props = {|
target?: "_blank",
as?: string | React.ComponentType<*>,
hide?: () => void,
+ level?: number,
|};
const MenuItem = ({
@@ -86,6 +87,7 @@ const Spacer = styled.svg`
export const MenuAnchor = styled.a`
display: flex;
margin: 0;
+ margin-left: ${(props) => props.level * 10}px;
border: 0;
padding: 12px;
width: 100%;
diff --git a/app/components/ContextMenu/Template.js b/app/components/ContextMenu/Template.js
index bbd3bd120..f8a2507ca 100644
--- a/app/components/ContextMenu/Template.js
+++ b/app/components/ContextMenu/Template.js
@@ -9,6 +9,7 @@ import {
MenuItem as BaseMenuItem,
} from "reakit/Menu";
import styled from "styled-components";
+import Header from "./Header";
import MenuItem, { MenuAnchor } from "./MenuItem";
import Separator from "./Separator";
import ContextMenu from ".";
@@ -34,6 +35,7 @@ type TMenuItem =
visible?: boolean,
selected?: boolean,
disabled?: boolean,
+ level?: number,
|}
| {|
title: React.Node,
@@ -128,7 +130,8 @@ function Template({ items, ...menu }: Props): React.Node {
key={index}
disabled={item.disabled}
selected={item.selected}
- target="_blank"
+ level={item.level}
+ target={item.href.startsWith("#") ? undefined : "_blank"}
{...menu}
>
{item.title}
@@ -167,6 +170,10 @@ function Template({ items, ...menu }: Props): React.Node {
return ;
}
+ if (item.type === "heading") {
+ return ;
+ }
+
return null;
});
}
diff --git a/app/menus/TableOfContentsMenu.js b/app/menus/TableOfContentsMenu.js
new file mode 100644
index 000000000..abe1f9780
--- /dev/null
+++ b/app/menus/TableOfContentsMenu.js
@@ -0,0 +1,66 @@
+// @flow
+import { observer } from "mobx-react";
+import { TableOfContentsIcon } from "outline-icons";
+import * as React from "react";
+import { useTranslation } from "react-i18next";
+import { MenuButton, useMenuState } from "reakit/Menu";
+import Button from "components/Button";
+import ContextMenu from "components/ContextMenu";
+import Template from "components/ContextMenu/Template";
+
+type Props = {|
+ headings: { title: string, level: number, id: string }[],
+|};
+
+function TableOfContentsMenu({ headings }: Props) {
+ const menu = useMenuState({
+ modal: true,
+ unstable_preventOverflow: true,
+ unstable_fixed: true,
+ unstable_flip: true,
+ });
+
+ const { t } = useTranslation();
+
+ const minHeading = headings.reduce(
+ (memo, heading) => (heading.level < memo ? heading.level : memo),
+ Infinity
+ );
+
+ return (
+ <>
+
+ {(props) => (
+ }
+ iconColor="currentColor"
+ borderOnHover
+ neutral
+ />
+ )}
+
+
+ {
+ return {
+ href: `#${heading.id}`,
+ title: t(heading.title),
+ level: heading.level - minHeading,
+ };
+ }),
+ ]}
+ />
+
+ >
+ );
+}
+
+export default observer(TableOfContentsMenu);
diff --git a/app/scenes/Document/components/Document.js b/app/scenes/Document/components/Document.js
index 726c4ee52..03a65ba7b 100644
--- a/app/scenes/Document/components/Document.js
+++ b/app/scenes/Document/components/Document.js
@@ -374,6 +374,7 @@ class DocumentScene extends React.Component {
sharedTree={this.props.sharedTree}
goBack={this.goBack}
onSave={this.onSave}
+ headings={headings}
/>
void,
+ headings: { title: string, level: number, id: string }[],
|};
function DocumentHeader({
@@ -60,6 +62,7 @@ function DocumentHeader({
publishingIsDisabled,
sharedTree,
onSave,
+ headings,
}: Props) {
const { t } = useTranslation();
const { auth, ui, policies } = useStores();
@@ -153,6 +156,11 @@ function DocumentHeader({
}
actions={
<>
+ {isMobile && (
+
+
+
+ )}
{!isPublishing && isSaving && {t("Saving")}…}
props.theme.slate};
`;
+const TocWrapper = styled(Action)`
+ position: absolute;
+ left: 42px;
+`;
+
export default observer(DocumentHeader);
diff --git a/shared/i18n/locales/en_US/translation.json b/shared/i18n/locales/en_US/translation.json
index 7d42aee8a..6b68be10d 100644
--- a/shared/i18n/locales/en_US/translation.json
+++ b/shared/i18n/locales/en_US/translation.json
@@ -206,6 +206,7 @@
"Share options": "Share options",
"Go to document": "Go to document",
"Revoke link": "Revoke link",
+ "Table of contents": "Table of contents",
"By {{ author }}": "By {{ author }}",
"Are you sure you want to make {{ userName }} an admin? Admins can modify team and billing information.": "Are you sure you want to make {{ userName }} an admin? Admins can modify team and billing information.",
"Are you sure you want to make {{ userName }} a member?": "Are you sure you want to make {{ userName }} a member?",
@@ -337,7 +338,6 @@
"Move current document": "Move current document",
"Jump to search": "Jump to search",
"Jump to home": "Jump to home",
- "Table of contents": "Table of contents",
"Toggle navigation": "Toggle navigation",
"Focus search input": "Focus search input",
"Open this guide": "Open this guide",