diff --git a/packages/icons/src/Icons/ic-toc.svg b/packages/icons/src/Icons/ic-toc.svg new file mode 100644 index 000000000..6c02cea2c --- /dev/null +++ b/packages/icons/src/Icons/ic-toc.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/icons/src/Icons/index.ts b/packages/icons/src/Icons/index.ts index c05c632d4..f8c78a9d5 100644 --- a/packages/icons/src/Icons/index.ts +++ b/packages/icons/src/Icons/index.ts @@ -214,6 +214,7 @@ import ViewIcon from './ic-view.svg' import WarningIcon from './ic-warning.svg' import WindowIcon from './ic-window.svg' import DetailsBlockIcon from './ic-details-block.svg' +import TableOfContentsIcon from './ic-toc.svg' export { AccessibilityIcon, @@ -408,6 +409,7 @@ export { SubtractIcon, SuperscriptIcon, SyncIcon, + TableOfContentsIcon, TasksIcon, TextCircleIcon, TextIcon, diff --git a/packages/web/src/javascripts/Components/Icon/IconNameToSvgMapping.tsx b/packages/web/src/javascripts/Components/Icon/IconNameToSvgMapping.tsx index 755abcf7b..48d602d27 100644 --- a/packages/web/src/javascripts/Components/Icon/IconNameToSvgMapping.tsx +++ b/packages/web/src/javascripts/Components/Icon/IconNameToSvgMapping.tsx @@ -142,6 +142,7 @@ export const IconNameToSvgMapping = { themes: icons.ThemesIcon, trash: icons.TrashIcon, tune: icons.TuneIcon, + toc: icons.TableOfContentsIcon, unarchive: icons.UnarchiveIcon, underline: icons.UnderlineIcon, undo: icons.UndoIcon, diff --git a/packages/web/src/javascripts/Components/StyledTooltip/StyledTooltip.tsx b/packages/web/src/javascripts/Components/StyledTooltip/StyledTooltip.tsx index a3bfe5d4d..1976fbf21 100644 --- a/packages/web/src/javascripts/Components/StyledTooltip/StyledTooltip.tsx +++ b/packages/web/src/javascripts/Components/StyledTooltip/StyledTooltip.tsx @@ -67,7 +67,7 @@ const StyledTooltip = ({ const clickProps = isMobile ? {} : { - onClick: () => setForceOpen(false), + onClick: () => tooltip.hide(), } useEffect(() => { diff --git a/packages/web/src/javascripts/Components/SuperEditor/Plugins/ToolbarPlugin/ToolbarPlugin.tsx b/packages/web/src/javascripts/Components/SuperEditor/Plugins/ToolbarPlugin/ToolbarPlugin.tsx index bd3509986..3018214e9 100644 --- a/packages/web/src/javascripts/Components/SuperEditor/Plugins/ToolbarPlugin/ToolbarPlugin.tsx +++ b/packages/web/src/javascripts/Components/SuperEditor/Plugins/ToolbarPlugin/ToolbarPlugin.tsx @@ -25,7 +25,7 @@ import { mergeRegister, $findMatchingParent, $getNearestNodeOfType } from '@lexi import { $isLinkNode, $isAutoLinkNode, TOGGLE_LINK_COMMAND } from '@lexical/link' import { $isListNode, ListNode } from '@lexical/list' import { $isHeadingNode } from '@lexical/rich-text' -import { ComponentPropsWithoutRef, useCallback, useEffect, useRef, useState } from 'react' +import { ComponentPropsWithoutRef, ForwardedRef, forwardRef, useCallback, useEffect, useRef, useState } from 'react' import { CenterAlignBlock, JustifyAlignBlock, LeftAlignBlock, RightAlignBlock } from '../Blocks/Alignment' import { BulletedListBlock, ChecklistBlock, NumberedListBlock } from '../Blocks/List' import { CodeBlock } from '../Blocks/Code' @@ -46,6 +46,10 @@ import { PasswordBlock } from '../Blocks/Password' import LinkEditor from './ToolbarLinkEditor' import { FOCUSABLE_BUT_NOT_TABBABLE, URL_REGEX } from '@/Constants/Constants' import LinkTextEditor, { $isLinkTextNode } from './ToolbarLinkTextEditor' +import Popover from '@/Components/Popover/Popover' +import LexicalTableOfContents from '@lexical/react/LexicalTableOfContents' +import Menu from '@/Components/Menu/Menu' +import MenuItem from '@/Components/Menu/MenuItem' const TOGGLE_LINK_AND_EDIT_COMMAND = createCommand('TOGGLE_LINK_AND_EDIT_COMMAND') @@ -71,36 +75,42 @@ interface ToolbarButtonProps extends ComponentPropsWithoutRef<'button'> { onSelect: () => void } -const ToolbarButton = ({ name, active, iconName, onSelect, disabled, ...props }: ToolbarButtonProps) => { - const [editor] = useLexicalComposerContext() +const ToolbarButton = forwardRef( + ( + { name, active, iconName, onSelect, disabled, ...props }: ToolbarButtonProps, + ref: ForwardedRef, + ) => { + const [editor] = useLexicalComposerContext() - return ( - - { - event.preventDefault() - onSelect() - }} - onContextMenu={(event) => { - editor.focus() - event.preventDefault() - }} - disabled={disabled} - {...props} - > -
+ { + event.preventDefault() + onSelect() + }} + onContextMenu={(event) => { + editor.focus() + event.preventDefault() + }} + disabled={disabled} + ref={ref} + {...props} > - -
-
-
- ) -} +
+ +
+ + + ) + }, +) const ToolbarPlugin = () => { const application = useApplication() @@ -132,6 +142,9 @@ const ToolbarPlugin = () => { const [linkText, setLinkText] = useState('') const [linkUrl, setLinkUrl] = useState('') + const [isTOCOpen, setIsTOCOpen] = useState(false) + const tocAnchorRef = useRef(null) + const [canUndo, setCanUndo] = useState(false) const [canRedo, setCanRedo] = useState(false) @@ -451,6 +464,13 @@ const ToolbarPlugin = () => { ref={toolbarRef} store={toolbarStore} > + setIsTOCOpen(!isTOCOpen)} + ref={tocAnchorRef} + /> { )} + setIsTOCOpen(!isTOCOpen)} + side="top" + align="center" + className="py-1" + disableMobileFullscreenTakeover + > +
Table of Contents
+ + {(tableOfContents) => { + if (!tableOfContents.length) { + return
No headings found
+ } + + return ( + + {tableOfContents.map(([key, text, tag]) => ( + { + setIsTOCOpen(false) + editor.getEditorState().read(() => { + const domElement = editor.getElementByKey(key) + if (!domElement) { + return + } + const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches + domElement.scrollIntoView({ behavior: reducedMotion ? 'auto' : 'smooth', block: 'nearest' }) + editor.focus() + }) + }} + > + + {text} + + ))} + + ) + }} +
+
) }