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'
|
'use client'
|
||||||
|
|
||||||
import { useEffect, useRef, useState, useCallback, forwardRef, useImperativeHandle } from 'react'
|
import { useEffect, useRef, useState, useCallback, forwardRef, useImperativeHandle } from 'react'
|
||||||
|
import { useLanguage } from '@/lib/i18n'
|
||||||
import { useEditor, EditorContent } from '@tiptap/react'
|
import { useEditor, EditorContent } from '@tiptap/react'
|
||||||
import { BubbleMenu } from '@tiptap/react/menus'
|
import { BubbleMenu } from '@tiptap/react/menus'
|
||||||
import StarterKit from '@tiptap/starter-kit'
|
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 }) {
|
function SlashCommandMenu({ editor, onInsertImage }: { editor: Editor; onInsertImage: (editor: Editor) => void }) {
|
||||||
|
const { t } = useLanguage()
|
||||||
const [isOpen, setIsOpen] = useState(false)
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
const [query, setQuery] = useState('')
|
const [query, setQuery] = useState('')
|
||||||
const [selectedIndex, setSelectedIndex] = useState(0)
|
const [selectedIndex, setSelectedIndex] = useState(0)
|
||||||
@@ -332,6 +334,38 @@ function SlashCommandMenu({ editor, onInsertImage }: { editor: Editor; onInsertI
|
|||||||
const menuRef = useRef<HTMLDivElement>(null)
|
const menuRef = useRef<HTMLDivElement>(null)
|
||||||
const selectedItemRef = useRef<HTMLButtonElement>(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(() => {
|
const closeMenu = useCallback(() => {
|
||||||
setIsOpen(false); setQuery(''); setSelectedIndex(0); setActiveCategory(null)
|
setIsOpen(false); setQuery(''); setSelectedIndex(0); setActiveCategory(null)
|
||||||
}, [])
|
}, [])
|
||||||
@@ -364,10 +398,9 @@ function SlashCommandMenu({ editor, onInsertImage }: { editor: Editor; onInsertI
|
|||||||
}, [editor, closeMenu, deleteSlashText, onInsertImage])
|
}, [editor, closeMenu, deleteSlashText, onInsertImage])
|
||||||
|
|
||||||
// All category names in order
|
// 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 = localCommands.filter(c => c.title.toLowerCase().includes(query.toLowerCase()) || c.description.toLowerCase().includes(query.toLowerCase()))
|
||||||
const textFiltered = slashCommands.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 filtered = activeCategory ? textFiltered.filter(c => (c.category || 'Basic blocks') === activeCategory) : textFiltered
|
||||||
|
|
||||||
const categories = filtered.reduce((acc, item) => {
|
const categories = filtered.reduce((acc, item) => {
|
||||||
@@ -413,7 +446,11 @@ function SlashCommandMenu({ editor, onInsertImage }: { editor: Editor; onInsertI
|
|||||||
}, [isOpen, editor, query])
|
}, [isOpen, editor, query])
|
||||||
|
|
||||||
useEffect(() => {
|
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)
|
document.addEventListener('click', handleClick)
|
||||||
return () => document.removeEventListener('click', handleClick)
|
return () => document.removeEventListener('click', handleClick)
|
||||||
}, [isOpen, closeMenu])
|
}, [isOpen, closeMenu])
|
||||||
@@ -447,7 +484,7 @@ function SlashCommandMenu({ editor, onInsertImage }: { editor: Editor; onInsertI
|
|||||||
onMouseDown={(e) => e.preventDefault()}
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
onClick={() => { setActiveCategory(null); setSelectedIndex(0) }}
|
onClick={() => { setActiveCategory(null); setSelectedIndex(0) }}
|
||||||
>
|
>
|
||||||
Tout
|
{t('richTextEditor.slashTabAll')}
|
||||||
</button>
|
</button>
|
||||||
{allCategories.filter(cat => categories[cat]).map(cat => (
|
{allCategories.filter(cat => categories[cat]).map(cat => (
|
||||||
<button
|
<button
|
||||||
@@ -466,20 +503,20 @@ function SlashCommandMenu({ editor, onInsertImage }: { editor: Editor; onInsertI
|
|||||||
|
|
||||||
{/* Header hint */}
|
{/* Header hint */}
|
||||||
<div className="notion-slash-header">
|
<div className="notion-slash-header">
|
||||||
<span>↑↓ naviguer · Entrée insérer · Tab changer de section</span>
|
<span>{t('richTextEditor.slashHint')}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{aiLoading && (
|
{aiLoading && (
|
||||||
<div className="notion-slash-loading">
|
<div className="notion-slash-loading">
|
||||||
<Sparkles className="w-3.5 h-3.5 animate-spin" />
|
<Sparkles className="w-3.5 h-3.5 animate-spin" />
|
||||||
<span>IA Note réfléchit...</span>
|
<span>{t('richTextEditor.slashLoading')}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!aiLoading && Object.entries(categories).map(([cat, items]) => (
|
{!aiLoading && Object.entries(categories).map(([cat, items]) => (
|
||||||
<div key={cat} className="notion-slash-section">
|
<div key={cat} className="notion-slash-section">
|
||||||
<div className={cn('notion-slash-label', cat === 'IA Note' && 'notion-slash-label-ai')}>
|
<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 === 'IA Note' && <Sparkles className="w-3 h-3 inline mr-1" />}
|
||||||
{cat}
|
{CAT_LABELS[cat] || cat}
|
||||||
</div>
|
</div>
|
||||||
{items.map((item) => {
|
{items.map((item) => {
|
||||||
flatIndex++
|
flatIndex++
|
||||||
|
|||||||
@@ -450,6 +450,60 @@
|
|||||||
"generateTitleFromImage": "Générer un titre à partir de l'image",
|
"generateTitleFromImage": "Générer un titre à partir de l'image",
|
||||||
"titleGenerated": "Titre généré à 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": {
|
"titleSuggestions": {
|
||||||
"available": "Suggestions de titre",
|
"available": "Suggestions de titre",
|
||||||
"title": "Suggestions IA",
|
"title": "Suggestions IA",
|
||||||
|
|||||||
Reference in New Issue
Block a user