fix: slash menu tabs stay open (menuRef.contains check) + full i18n support
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 44s
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 44s
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useRef, useState, useCallback, forwardRef, useImperativeHandle } from 'react'
|
||||
import { useLanguage } from '@/lib/i18n'
|
||||
import { useEditor, EditorContent } from '@tiptap/react'
|
||||
import { BubbleMenu } from '@tiptap/react/menus'
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
@@ -323,6 +324,7 @@ function BubbleToolbar({ editor }: { editor: Editor | null }) {
|
||||
}
|
||||
|
||||
function SlashCommandMenu({ editor, onInsertImage }: { editor: Editor; onInsertImage: (editor: Editor) => void }) {
|
||||
const { t } = useLanguage()
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [query, setQuery] = useState('')
|
||||
const [selectedIndex, setSelectedIndex] = useState(0)
|
||||
@@ -332,6 +334,38 @@ function SlashCommandMenu({ editor, onInsertImage }: { editor: Editor; onInsertI
|
||||
const menuRef = useRef<HTMLDivElement>(null)
|
||||
const selectedItemRef = useRef<HTMLButtonElement>(null)
|
||||
|
||||
// Translated category names (keys match slashCommands category field)
|
||||
const CAT_LABELS: Record<string, string> = {
|
||||
'Basic blocks': t('richTextEditor.slashCatBasic'),
|
||||
'Media': t('richTextEditor.slashCatMedia'),
|
||||
'Formatting': t('richTextEditor.slashCatFormatting'),
|
||||
'IA Note': t('richTextEditor.slashCatAi'),
|
||||
}
|
||||
|
||||
// Translated command list (keeps same order/icons/shortcuts as global slashCommands)
|
||||
const localCommands: SlashItem[] = [
|
||||
{ ...slashCommands[0], title: t('richTextEditor.slashText'), description: t('richTextEditor.slashTextDesc'), category: 'Basic blocks' },
|
||||
{ ...slashCommands[1], title: t('richTextEditor.slashH1'), description: t('richTextEditor.slashH1Desc'), category: 'Basic blocks' },
|
||||
{ ...slashCommands[2], title: t('richTextEditor.slashH2'), description: t('richTextEditor.slashH2Desc'), category: 'Basic blocks' },
|
||||
{ ...slashCommands[3], title: t('richTextEditor.slashH3'), description: t('richTextEditor.slashH3Desc'), category: 'Basic blocks' },
|
||||
{ ...slashCommands[4], title: t('richTextEditor.slashBullet'), description: t('richTextEditor.slashBulletDesc'), category: 'Basic blocks' },
|
||||
{ ...slashCommands[5], title: t('richTextEditor.slashNumbered'), description: t('richTextEditor.slashNumberedDesc'), category: 'Basic blocks' },
|
||||
{ ...slashCommands[6], title: t('richTextEditor.slashTodo'), description: t('richTextEditor.slashTodoDesc'), category: 'Basic blocks' },
|
||||
{ ...slashCommands[7], title: t('richTextEditor.slashQuote'), description: t('richTextEditor.slashQuoteDesc'), category: 'Basic blocks' },
|
||||
{ ...slashCommands[8], title: t('richTextEditor.slashCode'), description: t('richTextEditor.slashCodeDesc'), category: 'Basic blocks' },
|
||||
{ ...slashCommands[9], title: t('richTextEditor.slashDivider'), description: t('richTextEditor.slashDividerDesc'), category: 'Basic blocks' },
|
||||
{ ...slashCommands[10], title: t('richTextEditor.slashImage'), description: t('richTextEditor.slashImageDesc'), category: 'Media' },
|
||||
{ ...slashCommands[11], title: t('richTextEditor.slashAlignLeft'), description: t('richTextEditor.slashAlignLeftDesc'), category: 'Formatting' },
|
||||
{ ...slashCommands[12], title: t('richTextEditor.slashAlignCenter'), description: t('richTextEditor.slashAlignCenterDesc'), category: 'Formatting' },
|
||||
{ ...slashCommands[13], title: t('richTextEditor.slashAlignRight'), description: t('richTextEditor.slashAlignRightDesc'), category: 'Formatting' },
|
||||
{ ...slashCommands[14], title: t('richTextEditor.slashSuperscript'), description: t('richTextEditor.slashSuperscriptDesc'), category: 'Formatting' },
|
||||
{ ...slashCommands[15], title: t('richTextEditor.slashSubscript'), description: t('richTextEditor.slashSubscriptDesc'), category: 'Formatting' },
|
||||
{ ...slashCommands[16], title: t('richTextEditor.slashClarify'), description: t('richTextEditor.slashClarifyDesc'), category: 'IA Note' },
|
||||
{ ...slashCommands[17], title: t('richTextEditor.slashShorten'), description: t('richTextEditor.slashShortenDesc'), category: 'IA Note' },
|
||||
{ ...slashCommands[18], title: t('richTextEditor.slashImprove'), description: t('richTextEditor.slashImproveDesc'), category: 'IA Note' },
|
||||
{ ...slashCommands[19], title: t('richTextEditor.slashExpand'), description: t('richTextEditor.slashExpandDesc'), category: 'IA Note' },
|
||||
]
|
||||
|
||||
const closeMenu = useCallback(() => {
|
||||
setIsOpen(false); setQuery(''); setSelectedIndex(0); setActiveCategory(null)
|
||||
}, [])
|
||||
@@ -364,10 +398,9 @@ function SlashCommandMenu({ editor, onInsertImage }: { editor: Editor; onInsertI
|
||||
}, [editor, closeMenu, deleteSlashText, onInsertImage])
|
||||
|
||||
// All category names in order
|
||||
const allCategories = Array.from(new Set(slashCommands.map(c => c.category || 'Basic blocks')))
|
||||
const allCategories = Array.from(new Set(localCommands.map(c => c.category || 'Basic blocks')))
|
||||
|
||||
// Filtered by text query, then optionally by active tab
|
||||
const textFiltered = slashCommands.filter(c => c.title.toLowerCase().includes(query.toLowerCase()) || c.description.toLowerCase().includes(query.toLowerCase()))
|
||||
const textFiltered = localCommands.filter(c => c.title.toLowerCase().includes(query.toLowerCase()) || c.description.toLowerCase().includes(query.toLowerCase()))
|
||||
const filtered = activeCategory ? textFiltered.filter(c => (c.category || 'Basic blocks') === activeCategory) : textFiltered
|
||||
|
||||
const categories = filtered.reduce((acc, item) => {
|
||||
@@ -413,7 +446,11 @@ function SlashCommandMenu({ editor, onInsertImage }: { editor: Editor; onInsertI
|
||||
}, [isOpen, editor, query])
|
||||
|
||||
useEffect(() => {
|
||||
const handleClick = () => { if (isOpen) closeMenu() }
|
||||
const handleClick = (e: MouseEvent) => {
|
||||
if (isOpen && menuRef.current && !menuRef.current.contains(e.target as Node)) {
|
||||
closeMenu()
|
||||
}
|
||||
}
|
||||
document.addEventListener('click', handleClick)
|
||||
return () => document.removeEventListener('click', handleClick)
|
||||
}, [isOpen, closeMenu])
|
||||
@@ -447,7 +484,7 @@ function SlashCommandMenu({ editor, onInsertImage }: { editor: Editor; onInsertI
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={() => { setActiveCategory(null); setSelectedIndex(0) }}
|
||||
>
|
||||
Tout
|
||||
{t('richTextEditor.slashTabAll')}
|
||||
</button>
|
||||
{allCategories.filter(cat => categories[cat]).map(cat => (
|
||||
<button
|
||||
@@ -466,20 +503,20 @@ function SlashCommandMenu({ editor, onInsertImage }: { editor: Editor; onInsertI
|
||||
|
||||
{/* Header hint */}
|
||||
<div className="notion-slash-header">
|
||||
<span>↑↓ naviguer · Entrée insérer · Tab changer de section</span>
|
||||
<span>{t('richTextEditor.slashHint')}</span>
|
||||
</div>
|
||||
|
||||
{aiLoading && (
|
||||
<div className="notion-slash-loading">
|
||||
<Sparkles className="w-3.5 h-3.5 animate-spin" />
|
||||
<span>IA Note réfléchit...</span>
|
||||
<span>{t('richTextEditor.slashLoading')}</span>
|
||||
</div>
|
||||
)}
|
||||
{!aiLoading && Object.entries(categories).map(([cat, items]) => (
|
||||
<div key={cat} className="notion-slash-section">
|
||||
<div className={cn('notion-slash-label', cat === 'IA Note' && 'notion-slash-label-ai')}>
|
||||
{cat === 'IA Note' && <Sparkles className="w-3 h-3 inline mr-1" />}
|
||||
{cat}
|
||||
{CAT_LABELS[cat] || cat}
|
||||
</div>
|
||||
{items.map((item) => {
|
||||
flatIndex++
|
||||
|
||||
Reference in New Issue
Block a user