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++
|
||||
|
||||
@@ -450,6 +450,60 @@
|
||||
"generateTitleFromImage": "Générer un titre à partir de l'image",
|
||||
"titleGenerated": "Titre généré à partir de l'image"
|
||||
},
|
||||
"richTextEditor": {
|
||||
"slashHint": "↑↓ naviguer · Entrée insérer · Tab changer de section",
|
||||
"slashLoading": "IA Note réfléchit...",
|
||||
"slashTabAll": "Tout",
|
||||
"slashCatBasic": "Blocs de base",
|
||||
"slashCatMedia": "Médias",
|
||||
"slashCatFormatting": "Mise en forme",
|
||||
"slashCatAi": "IA Note",
|
||||
"insertImage": "Insérer une image",
|
||||
"imageUrlPlaceholder": "https://exemple.com/image.png",
|
||||
"preview": "Aperçu",
|
||||
"cancel": "Annuler",
|
||||
"insert": "Insérer",
|
||||
"slashText": "Texte",
|
||||
"slashTextDesc": "Paragraphe simple",
|
||||
"slashH1": "Titre 1",
|
||||
"slashH1Desc": "Grand titre de section",
|
||||
"slashH2": "Titre 2",
|
||||
"slashH2Desc": "Titre de section moyen",
|
||||
"slashH3": "Titre 3",
|
||||
"slashH3Desc": "Petit titre de section",
|
||||
"slashBullet": "Liste à puces",
|
||||
"slashBulletDesc": "Liste non ordonnée",
|
||||
"slashNumbered": "Liste numérotée",
|
||||
"slashNumberedDesc": "Liste ordonnée numérotée",
|
||||
"slashTodo": "Liste de tâches",
|
||||
"slashTodoDesc": "Cases à cocher pour les tâches",
|
||||
"slashQuote": "Citation",
|
||||
"slashQuoteDesc": "Capturer une citation",
|
||||
"slashCode": "Bloc de code",
|
||||
"slashCodeDesc": "Extrait de code",
|
||||
"slashDivider": "Séparateur",
|
||||
"slashDividerDesc": "Séparateur horizontal",
|
||||
"slashImage": "Image",
|
||||
"slashImageDesc": "Intégrer une image depuis une URL",
|
||||
"slashAlignLeft": "Aligner à gauche",
|
||||
"slashAlignLeftDesc": "Aligner le texte à gauche",
|
||||
"slashAlignCenter": "Centrer",
|
||||
"slashAlignCenterDesc": "Centrer le texte",
|
||||
"slashAlignRight": "Aligner à droite",
|
||||
"slashAlignRightDesc": "Aligner le texte à droite",
|
||||
"slashSuperscript": "Exposant",
|
||||
"slashSuperscriptDesc": "Texte au-dessus de la ligne de base",
|
||||
"slashSubscript": "Indice",
|
||||
"slashSubscriptDesc": "Texte sous la ligne de base",
|
||||
"slashClarify": "Clarifier",
|
||||
"slashClarifyDesc": "Rendre le texte plus clair",
|
||||
"slashShorten": "Raccourcir",
|
||||
"slashShortenDesc": "Condenser le texte",
|
||||
"slashImprove": "Améliorer",
|
||||
"slashImproveDesc": "Améliorer le style",
|
||||
"slashExpand": "Développer",
|
||||
"slashExpandDesc": "Élaborer et enrichir le texte"
|
||||
},
|
||||
"titleSuggestions": {
|
||||
"available": "Suggestions de titre",
|
||||
"title": "Suggestions IA",
|
||||
|
||||
Reference in New Issue
Block a user