feat(editor): implement US-EDITOR-UX with global block selection, redesigned slash menu (favorites & preview), contextual placeholders, smart paste extended, turn into and undo/redo toasts
This commit is contained in:
122
memento-note/components/smart-paste-extended-menu.tsx
Normal file
122
memento-note/components/smart-paste-extended-menu.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useLanguage } from '@/lib/i18n'
|
||||
import { Link2, ImageIcon, Video, Code, FileText } from 'lucide-react'
|
||||
|
||||
export type SmartPasteExtendedMenuProps = {
|
||||
type: 'url' | 'code'
|
||||
text: string
|
||||
anchor: { top: number; left: number }
|
||||
isImage?: boolean
|
||||
isVideo?: boolean
|
||||
onLink?: () => void
|
||||
onImage?: () => void
|
||||
onVideo?: () => void
|
||||
onCodeBlock?: () => void
|
||||
onPlain: () => void
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export function SmartPasteExtendedMenu({
|
||||
type,
|
||||
text,
|
||||
anchor,
|
||||
isImage,
|
||||
isVideo,
|
||||
onLink,
|
||||
onImage,
|
||||
onVideo,
|
||||
onCodeBlock,
|
||||
onPlain,
|
||||
onClose,
|
||||
}: SmartPasteExtendedMenuProps) {
|
||||
const { t } = useLanguage()
|
||||
const menuRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
function handleClickOutside(e: MouseEvent) {
|
||||
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
function handleKeyDown(e: KeyboardEvent) {
|
||||
if (e.key === 'Escape') onClose()
|
||||
}
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
document.addEventListener('keydown', handleKeyDown)
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside)
|
||||
document.removeEventListener('keydown', handleKeyDown)
|
||||
}
|
||||
}, [onClose])
|
||||
|
||||
const menuStyle: React.CSSProperties = {
|
||||
position: 'fixed',
|
||||
left: anchor.left,
|
||||
top: anchor.top + 8,
|
||||
zIndex: 9999,
|
||||
maxWidth: 340,
|
||||
width: '100%',
|
||||
}
|
||||
|
||||
if (Number(menuStyle.left) + 340 > window.innerWidth) {
|
||||
menuStyle.left = Math.max(8, window.innerWidth - 348)
|
||||
}
|
||||
// Estimation de hauteur pour éviter les débordements
|
||||
const expectedHeight = type === 'url' ? (200) : (140)
|
||||
if (Number(menuStyle.top) + expectedHeight > window.innerHeight) {
|
||||
menuStyle.top = Math.max(8, anchor.top - expectedHeight - 8)
|
||||
}
|
||||
|
||||
const shortenedText = text.length > 50 ? `${text.slice(0, 47)}...` : text
|
||||
|
||||
return createPortal(
|
||||
<div ref={menuRef} style={menuStyle} className="block-action-menu smart-paste-menu">
|
||||
<p className="smart-paste-menu__hint font-semibold">
|
||||
{type === 'url' ? t('richTextEditor.smartPasteUrlTitle') || 'Lien ou Média détecté' : t('richTextEditor.smartPasteCodeTitle') || 'Code source détecté'}
|
||||
</p>
|
||||
<p className="smart-paste-menu__source text-xs truncate max-w-full" title={text}>
|
||||
{shortenedText}
|
||||
</p>
|
||||
<p className="text-[10px] text-muted-foreground px-3 mb-2">
|
||||
{type === 'url' ? t('richTextEditor.smartPasteUrlHint') || 'Que souhaitez-vous faire avec ce lien ?' : t('richTextEditor.smartPasteCodeHint') || 'Du code source a été détecté. Souhaitez-vous l\'insérer comme bloc de code ?'}
|
||||
</p>
|
||||
|
||||
{type === 'url' && (
|
||||
<>
|
||||
<button type="button" className="block-action-item" onClick={onLink}>
|
||||
<Link2 size={15} className="text-blue-500" />
|
||||
<span>{t('richTextEditor.smartPasteUrlLink') || 'Coller comme lien hypertexte'}</span>
|
||||
</button>
|
||||
{isImage && onImage && (
|
||||
<button type="button" className="block-action-item" onClick={onImage}>
|
||||
<ImageIcon size={15} className="text-emerald-500" />
|
||||
<span>{t('richTextEditor.smartPasteUrlImage') || 'Insérer comme image'}</span>
|
||||
</button>
|
||||
)}
|
||||
{isVideo && onVideo && (
|
||||
<button type="button" className="block-action-item" onClick={onVideo}>
|
||||
<Video size={15} className="text-rose-500" />
|
||||
<span>{t('richTextEditor.smartPasteUrlVideo') || 'Insérer comme lecteur vidéo'}</span>
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{type === 'code' && onCodeBlock && (
|
||||
<button type="button" className="block-action-item" onClick={onCodeBlock}>
|
||||
<Code size={15} className="text-purple-500" />
|
||||
<span>{t('richTextEditor.smartPasteCodeBlock') || 'Insérer comme bloc de code'}</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button type="button" className="block-action-item border-t border-border/50 mt-1 pt-1" onClick={onPlain}>
|
||||
<FileText size={15} className="text-muted-foreground" />
|
||||
<span>{t('richTextEditor.smartPastePlain') || 'Coller en texte brut'}</span>
|
||||
</button>
|
||||
</div>,
|
||||
document.body,
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user