Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to get the content of TOC #138

Open
condorheroblog opened this issue Jan 8, 2025 · 6 comments
Open

How to get the content of TOC #138

condorheroblog opened this issue Jan 8, 2025 · 6 comments
Labels
enhancement New feature or request

Comments

@condorheroblog
Copy link
Contributor

I need the outline content to be on the left or right side of the editor, rather than inside the editor. Are there corresponding events exposed that I can use directly?

image
@hunghg255
Copy link
Owner

@condorheroblog
Copy link
Contributor Author

Should expose a method like this

image

@hunghg255
Copy link
Owner

Should expose a method like this

image

look great!

@condorheroblog
Copy link
Contributor Author

I don't quite understand the logic of this extension, so I can't help you implement it, it's up to you. Thank you.

@hunghg255 hunghg255 added the enhancement New feature or request label Jan 8, 2025
@mart11-22
Copy link

@condorheroblog This editor is built on top of Tiptap. You can create a custom hook to pull the ToC from the editor and then build a separate component to display it.

I built something similar for myself --

### useTableOfContents.ts (hook)
`import { Editor } from '@tiptap/react'
import { useCallback, useEffect, useState } from 'react'

export interface TableOfContentsItem {
level: number
text: string
id: string
}

export function useTableOfContents(editor: Editor | null) {
const [items, setItems] = useState<TableOfContentsItem[]>([])

const getItems = useCallback(() => {
if (!editor) return []

const headings: TableOfContentsItem[] = []
editor.state.doc.descendants((node, pos) => {
  if (node.type.name === 'heading') {
    headings.push({
      level: node.attrs.level,
      text: node.textContent,
      id: node.attrs.id || `heading-${pos}`
    })
  }
})
return headings

}, [editor])

useEffect(() => {
if (!editor) return

const updateItems = () => {
  setItems(getItems())
}

// Initial update
updateItems()

// Update on content change
editor.on('update', updateItems)

return () => {
  editor.off('update', updateItems)
}

}, [editor, getItems])

return items
} `

ToC (Component -- This is a floating component I created for testing but you should be able to edit it into whatever you want)

`import React, { useState } from 'react'
import { BookMarked } from 'lucide-react'
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { ScrollArea } from "@/components/ui/scroll-area"
import { useTableOfContents, TableOfContentsItem } from '@/hooks/useTableOfContents'
import { Editor } from '@tiptap/react'

interface TableOfContentsProps {
className?: string
editor: Editor | null
}

export function TableOfContents({ className, editor }: TableOfContentsProps) {
const [expanded, setExpanded] = useState(true)
const items = useTableOfContents(editor)

const handleItemClick = (id: string) => {
const element = document.getElementById(id)
element?.scrollIntoView({ behavior: 'smooth', block: 'start' })
}

const renderHeadingItem = (item: TableOfContentsItem, index: number) => {
const indentLevel = item.level - 1
const hasLeadingLine = item.level > 1

// Calculate padding based on a consistent progression
const getPadding = (level: number) => {
  switch(level) {
    case 1: return "pl-2"    // Base padding
    case 2: return "pl-8"    // 32px
    case 3: return "pl-12"   // 48px
    case 4: return "pl-16"   // 64px
    case 5: return "pl-20"   // 80px
    case 6: return "pl-24"   // 96px
    default: return "pl-2"
  }
}

return (
  <div key={index} className="relative">
    {hasLeadingLine && (
      <div 
        className="absolute left-2 top-0 bottom-0 border-l border-muted-foreground/20"
        style={{
          marginLeft: `${(indentLevel - 1) * 20}px`, // Adjusted to match new padding
        }}
      />
    )}
    <button
      onClick={() => handleItemClick(item.id)}
      className={cn(
        "relative block w-full text-left px-2 py-1 hover:bg-accent rounded-md transition-colors",
        "text-sm text-muted-foreground hover:text-foreground",
        {
          "font-bold": item.level === 1,
          [getPadding(item.level)]: true
        }
      )}
    >
      {hasLeadingLine && (
        <div 
          className="absolute w-2 h-[1px] bg-muted-foreground/20"
          style={{
            left: `${(indentLevel - 1) * 20 + 8}px`, // Adjusted to match new padding
            top: '50%',
          }}
        />
      )}
      {item.text}
    </button>
  </div>
)

}

return (
<>
{expanded ? (
<div className={cn(
"w-64 bg-background shadow-lg transition-all duration-300 ease-in-out",
"border border-border rounded-lg",
className
)}>


Contents


<Button
variant="ghost"
size="sm"
onClick={() => setExpanded(false)}
aria-label="Collapse table of contents"
>



<ScrollArea className="p-4" style={{ maxHeight: 'calc(100vh - 100px)' }}>

{items.map((item, index) => renderHeadingItem(item, index))}



) : (
<Button
variant="outline"
size="sm"
onClick={() => setExpanded(true)}
aria-label="Expand table of contents"
className="fixed bottom-4 right-4 shadow-md rounded-full p-3 bg-background z-50"
>


)}
</>
)
} `

Image

@condorheroblog
Copy link
Contributor Author

Good job👍

For the convenience of users, invite you to submit a PR😁

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

3 participants