| import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; |
| import { useCallback, useEffect, useState } from 'react'; |
| import { |
| FORMAT_TEXT_COMMAND, |
| $getSelection, |
| $isRangeSelection, |
| TextFormatType |
| } from 'lexical'; |
| import { |
| TextBoldIcon, |
| TextItalicIcon, |
| TextStrikethroughIcon, |
| MagicWand01Icon, |
| PaintBucketIcon |
| } from 'hugeicons-react'; |
| import { $patchStyleText, $getSelectionStyleValueForProperty } from '@lexical/selection'; |
|
|
| export default function HoverToolbar() { |
| const [editor] = useLexicalComposerContext(); |
| const [isBold, setIsBold] = useState(false); |
| const [isItalic, setIsItalic] = useState(false); |
| const [isStrikethrough, setIsStrikethrough] = useState(false); |
| const [position, setPosition] = useState({ top: -10000, left: -10000, visible: false }); |
| const [showColorPicker, setShowColorPicker] = useState(false); |
|
|
| const colors = [ |
| '#000000', '#FF0000', '#00FF00', '#0000FF', |
| '#FFA500', '#800080', '#008080', '#FF69B4' |
| ]; |
|
|
| const updateToolbar = useCallback(() => { |
| editor.getEditorState().read(() => { |
| const selection = $getSelection(); |
| |
| if ($isRangeSelection(selection) && !selection.isCollapsed()) { |
| const nativeSelection = window.getSelection(); |
| if (nativeSelection && nativeSelection.rangeCount > 0) { |
| const domRange = nativeSelection.getRangeAt(0); |
| const rect = domRange.getBoundingClientRect(); |
| |
| setPosition({ |
| top: rect.top - 40, |
| left: rect.left + (rect.width / 2) - 80, |
| visible: true |
| }); |
| |
| setIsBold(selection.hasFormat('bold')); |
| setIsItalic(selection.hasFormat('italic')); |
| setIsStrikethrough(selection.hasFormat('strikethrough')); |
| } |
| } else { |
| setPosition(p => ({ ...p, visible: false })); |
| } |
| }); |
| }, [editor]); |
|
|
| useEffect(() => { |
| const unregister = editor.registerUpdateListener(() => { |
| updateToolbar(); |
| }); |
| document.addEventListener('selectionchange', updateToolbar); |
| return () => { |
| unregister(); |
| document.removeEventListener('selectionchange', updateToolbar); |
| }; |
| }, [editor, updateToolbar]); |
|
|
| const applyFormat = (format: TextFormatType) => { |
| editor.dispatchCommand(FORMAT_TEXT_COMMAND, format); |
| }; |
|
|
| const applyPixelize = () => { |
| editor.update(() => { |
| const selection = $getSelection(); |
| if ($isRangeSelection(selection)) { |
| const currentFilter = $getSelectionStyleValueForProperty(selection, 'filter'); |
| if (currentFilter === 'blur(3px)') { |
| $patchStyleText(selection, { 'filter': '' }); |
| } else { |
| $patchStyleText(selection, { 'filter': 'blur(3px)' }); |
| } |
| } |
| }); |
| }; |
|
|
| const applyColor = (color: string) => { |
| editor.update(() => { |
| const selection = $getSelection(); |
| if ($isRangeSelection(selection)) { |
| $patchStyleText(selection, { 'color': color }); |
| } |
| }); |
| setShowColorPicker(false); |
| }; |
|
|
| if (!position.visible) return null; |
|
|
| return ( |
| <div |
| style={{ |
| position: 'fixed', |
| top: position.top, |
| left: position.left, |
| backgroundColor: 'var(--bg-panel)', |
| border: '1px solid var(--border-color)', |
| borderRadius: '6px', |
| padding: '4px', |
| display: 'flex', |
| gap: '4px', |
| boxShadow: '0 4px 12px rgba(0,0,0,0.1)', |
| zIndex: 1000, |
| alignItems: 'center' |
| }} |
| > |
| <button |
| style={{ padding: 4, background: isBold ? '#eee' : 'transparent', borderRadius: 4 }} |
| onClick={() => applyFormat('bold')} |
| > |
| <TextBoldIcon size={16} /> |
| </button> |
| <button |
| style={{ padding: 4, background: isItalic ? '#eee' : 'transparent', borderRadius: 4 }} |
| onClick={() => applyFormat('italic')} |
| > |
| <TextItalicIcon size={16} /> |
| </button> |
| <button |
| style={{ padding: 4, background: isStrikethrough ? '#eee' : 'transparent', borderRadius: 4 }} |
| onClick={() => applyFormat('strikethrough')} |
| > |
| <TextStrikethroughIcon size={16} /> |
| </button> |
| |
| <div style={{ width: 1, height: 16, backgroundColor: '#ddd', margin: '0 4px' }} /> |
| |
| <button |
| style={{ padding: 4, background: 'transparent', borderRadius: 4, display: 'flex', alignItems: 'center' }} |
| onClick={applyPixelize} |
| title="Pixelize / Blur" |
| > |
| <MagicWand01Icon size={16} /> |
| </button> |
| |
| <div style={{ position: 'relative', display: 'flex', alignItems: 'center' }}> |
| <button |
| onClick={() => setShowColorPicker(!showColorPicker)} |
| style={{ padding: 4, background: 'transparent', borderRadius: 4, display: 'flex', alignItems: 'center' }} |
| title="Text Color" |
| > |
| <PaintBucketIcon size={16} /> |
| </button> |
| |
| {showColorPicker && ( |
| <div style={{ |
| position: 'absolute', top: 32, left: 0, zIndex: 100, |
| background: 'var(--bg-panel)', border: '1px solid var(--border-color)', |
| borderRadius: '6px', padding: '12px', display: 'flex', flexDirection: 'column', gap: '8px', |
| boxShadow: '0 4px 12px rgba(0,0,0,0.1)', minWidth: 160 |
| }}> |
| <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '4px' }}> |
| {colors.map(c => ( |
| <div |
| key={c} |
| onClick={() => applyColor(c)} |
| style={{ width: 24, height: 24, background: c, borderRadius: '2px', cursor: 'pointer', border: '1px solid rgba(0,0,0,0.05)' }} |
| /> |
| ))} |
| </div> |
| |
| <div style={{ height: 1, backgroundColor: 'var(--border-color)', margin: '4px 0' }} /> |
| |
| <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}> |
| <input |
| type="color" |
| onChange={(e) => applyColor(e.target.value)} |
| style={{ width: 32, height: 32, border: 'none', background: 'none', padding: 0, cursor: 'pointer' }} |
| /> |
| <input |
| type="text" |
| placeholder="#000000" |
| style={{ flex: 1, fontSize: '11px', padding: '4px 8px', border: '1px solid var(--border-color)', borderRadius: '4px', outline: 'none' }} |
| onKeyDown={(e: any) => { if (e.key === 'Enter') applyColor(e.target.value); }} |
| /> |
| </div> |
| <div style={{ fontSize: '9px', opacity: 0.5, textAlign: 'center' }}>Enter to apply Hex</div> |
| </div> |
| )} |
| </div> |
| </div> |
| ); |
| } |
|
|