mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-01-27 22:49:56 -05:00
Extracted TableOfContents into a separate component (#22019)
ref https://linear.app/ghost/issue/AP-634/table-of-contents-in-reader-view - `TableOfContents` is now a separate component to keep `ArticleModal` simpler - Switched to using constants for styling different heading levels for better performance and maintainability
This commit is contained in:
parent
7bc1102cc6
commit
0f1d6167cf
3 changed files with 88 additions and 75 deletions
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tryghost/admin-x-activitypub",
|
||||
"version": "0.3.47",
|
||||
"version": "0.3.48",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
|
@ -16,6 +16,7 @@ import {useThreadForUser} from '../../hooks/useActivityPubQueries';
|
|||
|
||||
import APAvatar from '../global/APAvatar';
|
||||
import APReplyBox from '../global/APReplyBox';
|
||||
import TableOfContents, {TOCItem} from './TableOfContents';
|
||||
import getReadingTime from '../../utils/get-reading-time';
|
||||
|
||||
interface ArticleModalProps {
|
||||
|
@ -37,79 +38,6 @@ interface IframeWindow extends Window {
|
|||
resizeIframe?: () => void;
|
||||
}
|
||||
|
||||
interface TOCItem {
|
||||
id: string;
|
||||
text: string;
|
||||
level: number;
|
||||
element?: HTMLElement;
|
||||
}
|
||||
|
||||
const TableOfContents: React.FC<{
|
||||
items: TOCItem[];
|
||||
activeId: string | null;
|
||||
onItemClick: (id: string) => void;
|
||||
}> = ({items, onItemClick}) => {
|
||||
if (items.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const getLineWidth = (level: number) => {
|
||||
switch (level) {
|
||||
case 1:
|
||||
return 'w-3';
|
||||
case 2:
|
||||
return 'w-2';
|
||||
default:
|
||||
return 'w-1';
|
||||
}
|
||||
};
|
||||
|
||||
const getHeadingPadding = (level: number) => {
|
||||
switch (level) {
|
||||
case 1:
|
||||
return 'pl-2';
|
||||
case 2:
|
||||
return 'pl-6';
|
||||
default:
|
||||
return 'pl-10';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="absolute right-2 top-1/2 -translate-y-1/2 text-base">
|
||||
<Popover
|
||||
position='center'
|
||||
side='right'
|
||||
trigger={
|
||||
<div className="flex cursor-pointer flex-col items-end gap-2 rounded-md bg-white p-2 hover:bg-grey-75">
|
||||
{items.map(item => (
|
||||
<div
|
||||
key={item.id}
|
||||
className={`h-[2px] rounded-sm bg-grey-400 pr-1 transition-all ${getLineWidth(item.level)}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="w-[220px] p-4">
|
||||
<nav className="max-h-[60vh] overflow-y-auto">
|
||||
{items.map(item => (
|
||||
<button
|
||||
key={item.id}
|
||||
className={`block w-full cursor-pointer truncate rounded py-1 text-left text-grey-700 hover:bg-grey-75 hover:text-grey-900 ${getHeadingPadding(item.level)}`}
|
||||
type='button'
|
||||
onClick={() => onItemClick(item.id)}
|
||||
>
|
||||
{item.text}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ArticleBody: React.FC<{
|
||||
heading: string;
|
||||
image: string|undefined;
|
||||
|
@ -859,7 +787,6 @@ const ArticleModal: React.FC<ArticleModalProps> = ({
|
|||
<div className="!visible absolute inset-y-0 right-7 z-40 hidden lg:!block">
|
||||
<div className="sticky top-1/2 -translate-y-1/2">
|
||||
<TableOfContents
|
||||
activeId={activeHeadingId}
|
||||
items={tocItems}
|
||||
onItemClick={scrollToHeading}
|
||||
/>
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
import React from 'react';
|
||||
import {Popover} from '@tryghost/admin-x-design-system';
|
||||
|
||||
export interface TOCItem {
|
||||
id: string;
|
||||
text: string;
|
||||
level: number;
|
||||
element?: HTMLElement;
|
||||
}
|
||||
|
||||
interface TableOfContentsProps {
|
||||
items: TOCItem[];
|
||||
onItemClick: (id: string) => void;
|
||||
}
|
||||
|
||||
const LINE_WIDTHS = {
|
||||
1: 'w-3',
|
||||
2: 'w-2',
|
||||
3: 'w-1'
|
||||
} as const;
|
||||
|
||||
const HEADING_PADDINGS = {
|
||||
1: 'pl-2',
|
||||
2: 'pl-6',
|
||||
3: 'pl-10'
|
||||
} as const;
|
||||
|
||||
const TableOfContents: React.FC<TableOfContentsProps> = ({items, onItemClick}) => {
|
||||
if (items.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const getNormalizedLevel = (level: number) => {
|
||||
return Math.min(level, 3);
|
||||
};
|
||||
|
||||
const getLineWidth = (level: number) => {
|
||||
return LINE_WIDTHS[getNormalizedLevel(level) as keyof typeof LINE_WIDTHS];
|
||||
};
|
||||
|
||||
const getHeadingPadding = (level: number) => {
|
||||
return HEADING_PADDINGS[getNormalizedLevel(level) as keyof typeof HEADING_PADDINGS];
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='absolute right-2 top-1/2 -translate-y-1/2 text-base'>
|
||||
<Popover
|
||||
aria-label='Table of Contents'
|
||||
position='center'
|
||||
side='right'
|
||||
trigger={
|
||||
<div className='flex cursor-pointer flex-col items-end gap-2 rounded-md bg-white p-2 hover:bg-grey-75'>
|
||||
{items.map(item => (
|
||||
<div
|
||||
key={item.id}
|
||||
className={`h-[2px] rounded-sm bg-grey-400 pr-1 transition-all ${getLineWidth(item.level)}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className='w-[220px] p-4'>
|
||||
<nav
|
||||
aria-label='Table of contents navigation'
|
||||
className='max-h-[60vh] overflow-y-auto'
|
||||
role='navigation'
|
||||
>
|
||||
{items.map(item => (
|
||||
<button
|
||||
key={item.id}
|
||||
className={`block w-full cursor-pointer truncate rounded py-1 text-left text-grey-700 hover:bg-grey-75 hover:text-grey-900 ${getHeadingPadding(item.level)}`}
|
||||
title={item.text}
|
||||
type='button'
|
||||
onClick={() => onItemClick(item.id)}
|
||||
>
|
||||
{item.text}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TableOfContents;
|
Loading…
Add table
Reference in a new issue