') + updateToolbarState() + handleInput() + return + } + + if (command === 'insertUnorderedList' && isAlreadyInFormat('list')) { + document.execCommand('insertUnorderedList', false, null) + updateToolbarState() + handleInput() + return + } + + // 处理自定义待办事项 + if (command === 'insertTodoList') { + insertTodoList() + updateToolbarState() + return + } // 检查嵌套限制 if ((command === 'insertUnorderedList' || (command === 'formatBlock' && value === 'blockquote')) && isInListOrQuote()) { @@ -175,12 +205,20 @@ const insertQuote = () => { const br = document.createElement('br') quoteContainer.parentNode.insertBefore(br, quoteContainer.nextSibling) - // 聚焦到内容区域 - const newRange = document.createRange() - newRange.selectNodeContents(contentSpan) - newRange.collapse(false) - selection.removeAllRanges() - selection.addRange(newRange) + // 聚焦到内容区域(延迟执行,确保在handleToolClick处理完后再聚焦) + setTimeout(() => { + if (editorRef.value) { + const newRange = document.createRange() + newRange.selectNodeContents(contentSpan) + newRange.collapse(false) + const sel = window.getSelection() + sel.removeAllRanges() + sel.addRange(newRange) + + // 确保编辑器保持焦点状态 + editorRef.value.focus() + } + }, 0) // 添加事件监听器到内容区域,监听内容变化 const checkContent = () => { @@ -238,12 +276,24 @@ const insertTodoList = () => { const br = document.createElement('br') todoContainer.parentNode.insertBefore(br, todoContainer.nextSibling) - // 聚焦到内容区域 - const newRange = document.createRange() - newRange.selectNodeContents(contentSpan) - newRange.collapse(false) - selection.removeAllRanges() - selection.addRange(newRange) + // 聚焦到内容区域(延迟执行,确保在handleToolClick处理完后再聚焦) + setTimeout(() => { + if (editorRef.value) { + const newRange = document.createRange() + newRange.selectNodeContents(contentSpan) + newRange.collapse(false) + const sel = window.getSelection() + sel.removeAllRanges() + sel.addRange(newRange) + + // 重要:不要重新聚焦到编辑器,因为待办事项有自己的可编辑区域 + // 但我们仍需要确保当前可编辑区域有焦点 + contentSpan.focus() + + // 确保工具栏保持可见 + isToolbarVisible.value = true + } + }, 0) // 添加事件监听器到图标 icon.addEventListener('click', function () { @@ -275,6 +325,11 @@ const insertTodoList = () => { contentSpan.addEventListener('input', checkContent) contentSpan.addEventListener('blur', checkContent) + // 添加焦点事件监听器,确保工具栏在待办事项获得焦点时保持可见 + contentSpan.addEventListener('focus', () => { + isToolbarVisible.value = true + }) + // 监听回车键,创建同级待办事项 contentSpan.addEventListener('keydown', e => { if (e.key === 'Enter') { @@ -343,6 +398,11 @@ const insertTodoList = () => { newContentSpan.addEventListener('input', newCheckContent) newContentSpan.addEventListener('blur', newCheckContent) + // 添加焦点事件监听器,确保工具栏在待办事项获得焦点时保持可见 + newContentSpan.addEventListener('focus', () => { + isToolbarVisible.value = true + }) + // 监听新内容区域的回车键 newContentSpan.addEventListener('keydown', e => { if (e.key === 'Enter') { @@ -411,6 +471,11 @@ const insertTodoList = () => { nextContentSpan.addEventListener('input', nextCheckContent) nextContentSpan.addEventListener('blur', nextCheckContent) + // 添加焦点事件监听器,确保工具栏在待办事项获得焦点时保持可见 + nextContentSpan.addEventListener('focus', () => { + isToolbarVisible.value = true + }) + handleInput() } }) @@ -509,6 +574,80 @@ onMounted(() => { } }) +// 显示工具栏 +const showToolbar = () => { + isToolbarVisible.value = true +} + +// 隐藏工具栏 +const hideToolbar = () => { + // 不立即隐藏工具栏,而是通过handleToolbarFocusOut处理 + // 添加延迟以确保点击工具栏按钮时不会立即隐藏 + setTimeout(() => { + handleToolbarFocusOut() + }, 200) +} + +// 处理工具栏按钮点击事件 +const handleToolClick = (action, event) => { + // 阻止事件冒泡,防止触发编辑器失焦 + if (event) { + event.preventDefault() + event.stopPropagation() + } + + // 执行工具操作 + action() + + // 对于待办事项,不需要重新聚焦到编辑器,因为它有自己的可编辑区域 + // 其他工具需要重新聚焦到编辑器 + const isTodoAction = action === insertTodoList + if (!isTodoAction) { + // 重新聚焦到编辑器 + setTimeout(() => { + if (editorRef.value) { + editorRef.value.focus() + } + }, 0) + } else { + // 对于待办事项,确保工具栏保持可见 + isToolbarVisible.value = true + } +} + +// 保持工具栏可见 +const keepToolbarVisible = () => { + isToolbarVisible.value = true +} + +// 处理工具栏失焦 +const handleToolbarFocusOut = () => { + // 添加一个小延迟,以便处理工具栏按钮的点击事件 + setTimeout(() => { + // 检查焦点是否在工具栏上 + const activeElement = document.activeElement + const toolbarElement = document.querySelector('.toolbar') + + // 如果焦点不在工具栏上才隐藏 + if (!toolbarElement || !toolbarElement.contains(activeElement)) { + // 额外检查是否有待办事项的内容区域有焦点 + const todoContentElements = document.querySelectorAll('.todo-content') + let todoHasFocus = false + for (let i = 0; i < todoContentElements.length; i++) { + if (todoContentElements[i] === activeElement || todoContentElements[i].contains(activeElement)) { + todoHasFocus = true + break + } + } + + // 只有在没有待办事项获得焦点时才隐藏工具栏 + if (!todoHasFocus) { + isToolbarVisible.value = false + } + } + }, 200) // 增加延迟时间,确保有足够时间处理点击事件 +} + // 监听外部值变化 defineExpose({ getContent: () => content.value, @@ -531,11 +670,24 @@ defineExpose({ } .toolbar { + position: fixed; + bottom: 0; + left: 0; + right: 0; display: flex; padding: 8px 12px; - border-bottom: 1px solid var(--border); + border-top: 1px solid var(--border); background-color: var(--background-card); flex-shrink: 0; + transform: translateY(100%); + transition: transform 0.3s ease; + z-index: 1000; + /* 确保工具栏能正确获取焦点 */ + tabindex: 0; +} + +.toolbar.visible { + transform: translateY(0); } .toolbar-btn { @@ -549,6 +701,8 @@ defineExpose({ display: flex; align-items: center; justify-content: center; + /* 确保按钮能正确获取焦点 */ + tabindex: 0; } .toolbar-btn:hover { @@ -572,11 +726,11 @@ defineExpose({ .editor-content { flex: 1; - padding: 20px 10px; + padding: 0 10px 60px 10px; /* 添加底部内边距,防止内容被工具栏遮挡 */ outline: none; overflow-y: auto; - font-size: 16px; - line-height: 1.6; + font-size: var(--editor-font-size, 16px); + line-height: var(--editor-line-height, 1.6); color: var(--note-content); min-height: 200px; background-color: var(--background-card); @@ -584,9 +738,9 @@ defineExpose({ position: relative; /* 基准线样式 */ background-image: linear-gradient(to bottom, var(--border) 1px, transparent 1px); - background-size: 100% 25.6px; /* 16px * 1.6 = 25.6px */ + background-size: 100% calc(var(--editor-font-size, 16px) * var(--editor-line-height, 1.6)); /* var(--editor-font-size) * var(--editor-line-height) */ background-repeat: repeat-y; - background-position: 0 20px; + background-position: 0 calc((var(--editor-font-size, 16px) * var(--editor-line-height, 1.6) - var(--editor-font-size, 16px)) / 2); } .editor-content::before { @@ -607,17 +761,17 @@ defineExpose({ /* 优化段落样式,确保与基准线对齐 */ :deep(.editor-content p) { margin: 0 0 12px 0; - line-height: 1.6; + line-height: var(--editor-line-height, 1.6); letter-spacing: 0.3px; } /* 自定义内容样式 - 统一行高和间距 */ :deep(.editor-content h2) { - font-size: 20px; + font-size: var(--editor-font-size, 16px); font-weight: 600; margin: 0 0 12px 0; color: var(--note-title); - line-height: 1.6; + line-height: var(--editor-line-height, 1.6); letter-spacing: 0.3px; text-align: center; position: relative; @@ -630,32 +784,32 @@ defineExpose({ color: var(--text-secondary); background-color: var(--background-secondary); font-style: italic; - line-height: 1.6; + line-height: var(--editor-line-height, 1.6); } :deep(.quote-container) { position: relative; margin: 0 0 12px 0; - line-height: 1.6; + line-height: var(--editor-line-height, 1.6); } :deep(.quote-icon) { position: absolute; left: 0; top: 0; - width: 16px; - height: 16px; + width: var(--editor-font-size, 16px); + height: var(--editor-font-size, 16px); margin-top: 3px; } :deep(.quote-content) { border-left: 3px solid var(--primary); - padding: 0 16px 0 32px; - margin-left: 16px; + padding: 0 var(--editor-font-size, 16px) 0 32px; + margin-left: var(--editor-font-size, 16px); color: var(--text-secondary); background-color: var(--background-secondary); font-style: italic; - line-height: 1.6; + line-height: var(--editor-line-height, 1.6); } :deep(.editor-content ul), @@ -663,12 +817,12 @@ defineExpose({ margin: 0 0 12px 0; padding-left: 32px; position: relative; - line-height: 1.6; + line-height: var(--editor-line-height, 1.6); } :deep(.editor-content li) { margin: 0; - line-height: 1.6; + line-height: var(--editor-line-height, 1.6); padding: 0; } @@ -702,26 +856,26 @@ defineExpose({ :deep(.quote-container) { position: relative; margin: 0 0 12px 0; - line-height: 1.6; + line-height: var(--editor-line-height, 1.6); } :deep(.quote-icon) { position: absolute; left: 0; top: 0; - width: 16px; - height: 16px; + width: var(--editor-font-size, 16px); + height: var(--editor-font-size, 16px); margin-top: 3px; } :deep(.quote-content) { border-left: 3px solid var(--primary); - padding: 0 16px 0 32px; - margin-left: 16px; + padding: 0 var(--editor-font-size, 16px) 0 32px; + margin-left: var(--editor-font-size, 16px); color: var(--text-secondary); background-color: var(--background-secondary); font-style: italic; - line-height: 1.6; + line-height: var(--editor-line-height, 1.6); } /* 待办事项样式 */ @@ -729,13 +883,13 @@ defineExpose({ display: flex; align-items: center; margin: 0; - line-height: 1.6; + line-height: var(--editor-line-height, 1.6); } :deep(.todo-icon) { - width: 25px; - height: 25px; - margin-right: 2px; + width: calc(var(--editor-font-size, 16px) * 2); + height: calc(var(--editor-font-size, 16px) * 2); + margin-right: 3px; margin-top: 0; cursor: pointer; flex-shrink: 0; @@ -744,7 +898,7 @@ defineExpose({ :deep(.todo-content) { flex: 1; outline: none; - line-height: 1.6; + line-height: var(--editor-line-height, 1.6); padding: 0; margin: 0; } diff --git a/src/pages/NoteEditorPage.vue b/src/pages/NoteEditorPage.vue index d8c3e63..f527ffa 100644 --- a/src/pages/NoteEditorPage.vue +++ b/src/pages/NoteEditorPage.vue @@ -3,17 +3,18 @@