diff --git a/apps/frontend/src/components/editor/EditableCustomHTMLRenderer.tsx b/apps/frontend/src/components/editor/EditableCustomHTMLRenderer.tsx index 4c23173..3f1c516 100644 --- a/apps/frontend/src/components/editor/EditableCustomHTMLRenderer.tsx +++ b/apps/frontend/src/components/editor/EditableCustomHTMLRenderer.tsx @@ -31,7 +31,7 @@ export function EditableCustomHTMLRenderer({ // 工具条状态管理 const [showToolbar, setShowToolbar] = useState(false) - const [toolbarPosition, setToolbarPosition] = useState({ x: 0, y: 0 }) + const [hasTextSelection, setHasTextSelection] = useState(false) const [formatState, setFormatState] = useState({ isBold: false, color: '', @@ -108,28 +108,6 @@ export function EditableCustomHTMLRenderer({ return cleanedHTML }, []) - // 坐标转换:iframe 内选区坐标 → 页面坐标 - const updateToolbarPosition = useCallback(() => { - const iframe = iframeRef.current - if (!iframe) return - - const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document - if (!iframeDoc) return - - const selection = iframeDoc.getSelection() - if (!selection || selection.rangeCount === 0) return - - const range = selection.getRangeAt(0) - const rangeRect = range.getBoundingClientRect() - const iframeRect = iframe.getBoundingClientRect() - - // 计算工具条位置(选区上方 50px) - const x = iframeRect.left + rangeRect.left + (rangeRect.width / 2) - 100 // 工具条宽度约 200px,居中 - const y = iframeRect.top + rangeRect.top - 50 + window.scrollY - - setToolbarPosition({ x, y }) - }, []) - // 在失焦前保存选区(供外部工具条交互时恢复) const preserveSelection = useCallback(() => { const iframe = iframeRef.current @@ -290,9 +268,9 @@ export function EditableCustomHTMLRenderer({ syncHTMLChanges() detectFormatState() - // 应用格式后隐藏工具栏和所有面板 + // 应用格式后只关闭附加面板,工具条在编辑态保持常驻 interactingWithToolbarRef.current = false - setShowToolbar(false) + setShowToolbar(true) setShowColorPicker(false) setShowSizePicker(false) } catch (error) { @@ -390,7 +368,7 @@ export function EditableCustomHTMLRenderer({ detectFormatState() setShowColorPicker(false) setShowSizePicker(false) - setShowToolbar(false) + setShowToolbar(true) interactingWithToolbarRef.current = false // 记录最后使用的颜色 lastUsedColorRef.current = color @@ -447,7 +425,7 @@ export function EditableCustomHTMLRenderer({ detectFormatState() setShowColorPicker(false) setShowSizePicker(false) - setShowToolbar(false) + setShowToolbar(true) interactingWithToolbarRef.current = false savedSelectionRef.current = null } catch (error) { @@ -495,7 +473,7 @@ export function EditableCustomHTMLRenderer({ console.error('[applySize] 恢复选区失败:', error) interactingWithToolbarRef.current = false setShowSizePicker(false) - setShowToolbar(false) + setShowToolbar(true) return } } @@ -505,7 +483,7 @@ export function EditableCustomHTMLRenderer({ console.error('[applySize] 选区恢复后没有 range') interactingWithToolbarRef.current = false setShowSizePicker(false) - setShowToolbar(false) + setShowToolbar(true) return } @@ -516,7 +494,7 @@ export function EditableCustomHTMLRenderer({ console.warn('[applySize] 选区为空') interactingWithToolbarRef.current = false setShowSizePicker(false) - setShowToolbar(false) + setShowToolbar(true) return } @@ -528,17 +506,17 @@ export function EditableCustomHTMLRenderer({ syncHTMLChanges() detectFormatState() - // 应用字号后隐藏字号面板和工具栏 + // 应用字号后只隐藏字号面板,工具条在编辑态保持常驻 interactingWithToolbarRef.current = false setShowSizePicker(false) - setShowToolbar(false) + setShowToolbar(true) setShowColorPicker(false) savedSelectionRef.current = null } catch (error) { console.error('应用字号失败:', error) interactingWithToolbarRef.current = false setShowSizePicker(false) - setShowToolbar(false) + setShowToolbar(true) } }, [syncHTMLChanges, detectFormatState]) @@ -575,9 +553,9 @@ export function EditableCustomHTMLRenderer({ parentA.setAttribute('href', url) parentA.setAttribute('target', '_blank') } else { - // 用户取消,隐藏工具栏和所有面板 + // 用户取消时只关闭附加面板,工具条在编辑态保持常驻 interactingWithToolbarRef.current = false - setShowToolbar(false) + setShowToolbar(true) setShowColorPicker(false) setShowSizePicker(false) return @@ -599,9 +577,9 @@ export function EditableCustomHTMLRenderer({ link.setAttribute('target', '_blank') }) } else { - // 用户取消,隐藏工具栏和所有面板 + // 用户取消时只关闭附加面板,工具条在编辑态保持常驻 interactingWithToolbarRef.current = false - setShowToolbar(false) + setShowToolbar(true) setShowColorPicker(false) setShowSizePicker(false) return @@ -611,9 +589,9 @@ export function EditableCustomHTMLRenderer({ syncHTMLChanges() detectFormatState() - // 应用链接后隐藏工具栏和所有面板 + // 应用链接后只关闭附加面板,工具条在编辑态保持常驻 interactingWithToolbarRef.current = false - setShowToolbar(false) + setShowToolbar(true) setShowColorPicker(false) setShowSizePicker(false) } catch (error) { @@ -721,48 +699,75 @@ export function EditableCustomHTMLRenderer({ // 注入编辑功能(整个 body 可编辑) const bodyElement = iframeDoc.body - // 单击模块进入编辑模式 - bodyElement.addEventListener('click', (e: Event) => { - const target = e.target as HTMLElement - - // 跳过图片点击(图片有自己的选择器) - if (target.tagName === 'IMG') return - - // 如果已经在编辑中,不重复处理 + const handleTextBlur = () => { + // 若正在与外部工具条交互,则不要退出编辑,立即恢复焦点 + if (interactingWithToolbarRef.current) { + setTimeout(() => { + bodyElement.contentEditable = 'true' + bodyElement.focus() + }, 0) + return + } + + bodyElement.contentEditable = 'false' + bodyElement.classList.remove('editing-text') + editingElementRef.current = null + setHasTextSelection(false) + setShowToolbar(false) + setShowColorPicker(false) + setShowSizePicker(false) + + // 同步修改 + syncHTMLChanges() + + bodyElement.removeEventListener('blur', handleTextBlur) + } + + const activateTextEditing = () => { if (bodyElement.contentEditable === 'true') return - - // 阻止事件冒泡,避免触发外部点击 - e.stopPropagation() // 整个 body 设置为可编辑 bodyElement.contentEditable = 'true' bodyElement.focus() bodyElement.classList.add('editing-text') editingElementRef.current = bodyElement + setHasTextSelection(false) + setShowToolbar(true) - // 失焦保存 - const handleBlur = () => { - // 若正在与外部工具条交互,则不要退出编辑,立即恢复焦点 - if (interactingWithToolbarRef.current) { - setTimeout(() => { - bodyElement.contentEditable = 'true' - bodyElement.focus() - }, 0) - return - } + bodyElement.removeEventListener('blur', handleTextBlur) + bodyElement.addEventListener('blur', handleTextBlur) + } - bodyElement.contentEditable = 'false' - bodyElement.classList.remove('editing-text') - editingElementRef.current = null + // 拖选文字时,selectionchange 会早于 click;先在 mousedown 激活编辑态。 + const handleTextMouseDown = (e: Event) => { + const target = e.target as HTMLElement - // 同步修改 - syncHTMLChanges() + // 跳过图片点击(图片有自己的选择器) + if (target.tagName === 'IMG') return - bodyElement.removeEventListener('blur', handleBlur) + // 阻止事件冒泡,避免触发外部点击 + if (bodyElement.contentEditable !== 'true') { + e.stopPropagation() } - bodyElement.addEventListener('blur', handleBlur) - }) + activateTextEditing() + } + + const handleTextClick = (e: Event) => { + const target = e.target as HTMLElement + + // 跳过图片点击(图片有自己的选择器) + if (target.tagName === 'IMG') return + + // 保留原来的单击激活入口,兼容不发生 mousedown 的触发方式。 + if (bodyElement.contentEditable !== 'true') { + e.stopPropagation() + activateTextEditing() + } + } + + bodyElement.addEventListener('mousedown', handleTextMouseDown) + bodyElement.addEventListener('click', handleTextClick) // 为所有图片添加点击事件 const images = iframeDoc.querySelectorAll('img') @@ -817,9 +822,12 @@ export function EditableCustomHTMLRenderer({ if (!selection || selection.rangeCount === 0) { console.log('[selectionchange] 没有选区') if (interactingWithToolbarRef.current) return - setShowToolbar(false) + setHasTextSelection(false) setShowColorPicker(false) setShowSizePicker(false) + if (bodyElement.contentEditable !== 'true') { + setShowToolbar(false) + } return } @@ -831,15 +839,20 @@ export function EditableCustomHTMLRenderer({ setShowColorPicker(false) setShowSizePicker(false) } + setHasTextSelection(true) setShowToolbar(true) - updateToolbarPosition() detectFormatState() } else { console.log('[selectionchange] 选区无效或不可编辑,隐藏工具栏') if (interactingWithToolbarRef.current) return - setShowToolbar(false) + setHasTextSelection(false) setShowColorPicker(false) setShowSizePicker(false) + if (bodyElement.contentEditable !== 'true') { + setShowToolbar(false) + } else { + setShowToolbar(true) + } } } @@ -861,10 +874,13 @@ export function EditableCustomHTMLRenderer({ // 如果没有选中文字,隐藏工具栏和所有面板 if (!selectedText) { - console.log('[click] 点击空白区域,隐藏工具栏') - setShowToolbar(false) + console.log('[click] 点击空白区域,清除选区状态') + setHasTextSelection(false) setShowColorPicker(false) setShowSizePicker(false) + if (bodyElement.contentEditable !== 'true') { + setShowToolbar(false) + } } } @@ -872,10 +888,13 @@ export function EditableCustomHTMLRenderer({ return () => { observer.disconnect() + bodyElement.removeEventListener('mousedown', handleTextMouseDown) + bodyElement.removeEventListener('click', handleTextClick) + bodyElement.removeEventListener('blur', handleTextBlur) iframeDoc.removeEventListener('selectionchange', handleSelectionChange) iframeDoc.removeEventListener('click', handleClick) } - }, [html, isEditing, syncHTMLChanges, updateToolbarPosition, detectFormatState, currentPage?.device_type, onContentWidthChange]) + }, [html, isEditing, syncHTMLChanges, detectFormatState, currentPage?.device_type, onContentWidthChange]) return ( <> @@ -889,21 +908,27 @@ export function EditableCustomHTMLRenderer({ {/* 浮动工具条 */} {showToolbar && (