优化了编辑器的格式

This commit is contained in:
2025-10-10 22:54:14 +08:00
parent 61a058eab3
commit 00c4fdee95

View File

@@ -67,8 +67,60 @@ const tools = ref([
}, },
]) ])
// 检查当前选区是否已经在某种格式中
const isAlreadyInFormat = formatType => {
const selection = window.getSelection()
if (selection.rangeCount > 0) {
const range = selection.getRangeAt(0)
const container = range.commonAncestorContainer
// 向上查找父元素,检查是否已经在指定格式中
let current = container.nodeType === Node.TEXT_NODE ? container.parentElement : container
while (current && current !== editorRef.value) {
if (formatType === 'bold' && current.tagName === 'B') return true
if (formatType === 'center' && current.style.textAlign === 'center') return true
if (formatType === 'header' && current.tagName === 'H2') return true
if (formatType === 'quote' && (current.tagName === 'BLOCKQUOTE' || current.classList.contains('quote-content'))) return true
if (formatType === 'list' && (current.tagName === 'UL' || current.tagName === 'OL' || current.tagName === 'LI')) return true
current = current.parentElement
}
}
return false
}
// 检查是否在列表、引用或待办事项中(用于嵌套限制)
const isInListOrQuote = () => {
const selection = window.getSelection()
if (selection.rangeCount > 0) {
const range = selection.getRangeAt(0)
const container = range.commonAncestorContainer
// 向上查找父元素,检查是否在列表、引用或待办事项中
let current = container.nodeType === Node.TEXT_NODE ? container.parentElement : container
while (current && current !== editorRef.value) {
if (current.tagName === 'UL' || current.tagName === 'OL' || current.tagName === 'LI' || current.tagName === 'BLOCKQUOTE' || current.classList.contains('quote-content') || current.classList.contains('todo-container')) {
return true
}
current = current.parentElement
}
}
return false
}
// 格式化文本 // 格式化文本
const formatText = (command, value = null) => { const formatText = (command, value = null) => {
// 检查是否已经应用了相同的格式
if (command === 'bold' && isAlreadyInFormat('bold')) return
if (command === 'justifyCenter' && isAlreadyInFormat('center')) return
if (command === 'formatBlock' && value === 'h2' && isAlreadyInFormat('header')) return
if (command === 'formatBlock' && value === 'blockquote' && isAlreadyInFormat('quote')) return
if (command === 'insertUnorderedList' && isAlreadyInFormat('list')) return
// 检查嵌套限制
if ((command === 'insertUnorderedList' || (command === 'formatBlock' && value === 'blockquote')) && isInListOrQuote()) {
return
}
document.execCommand(command, false, value) document.execCommand(command, false, value)
updateToolbarState() updateToolbarState()
handleInput() handleInput()
@@ -85,40 +137,66 @@ const insertQuote = () => {
const selection = window.getSelection() const selection = window.getSelection()
if (selection.rangeCount > 0) { if (selection.rangeCount > 0) {
const range = selection.getRangeAt(0) const range = selection.getRangeAt(0)
// 检查是否已经在引用中
const container = range.commonAncestorContainer
let current = container.nodeType === Node.TEXT_NODE ? container.parentElement : container
while (current && current !== editorRef.value) {
if (current.tagName === 'BLOCKQUOTE' || (current.classList && current.classList.contains('quote-content'))) return
current = current.parentElement
}
// 检查嵌套限制
if (isInListOrQuote()) return
// 创建引用容器 // 创建引用容器
const quoteContainer = document.createElement('div') const quoteContainer = document.createElement('div')
quoteContainer.className = 'quote-container' quoteContainer.className = 'quote-container'
// 创建图标元素 // 创建图标元素
const icon = document.createElement('img') const icon = document.createElement('img')
icon.className = 'quote-icon' icon.className = 'quote-icon'
icon.src = '/assets/icons/drawable-xxhdpi/rag_quote.png' icon.src = '/assets/icons/drawable-xxhdpi/rag_quote.png'
icon.alt = '引用' icon.alt = '引用'
// 创建内容容器 // 创建内容容器
const contentSpan = document.createElement('blockquote') const contentSpan = document.createElement('blockquote')
contentSpan.className = 'quote-content' contentSpan.className = 'quote-content'
contentSpan.textContent = '引用内容' contentSpan.textContent = '引用内容'
// 组装元素 // 组装元素
quoteContainer.appendChild(icon) quoteContainer.appendChild(icon)
quoteContainer.appendChild(contentSpan) quoteContainer.appendChild(contentSpan)
// 插入到当前光标位置 // 插入到当前光标位置
range.insertNode(quoteContainer) range.insertNode(quoteContainer)
// 添加换行 // 添加换行
const br = document.createElement('br') const br = document.createElement('br')
quoteContainer.parentNode.insertBefore(br, quoteContainer.nextSibling) quoteContainer.parentNode.insertBefore(br, quoteContainer.nextSibling)
// 聚焦到内容区域 // 聚焦到内容区域
const newRange = document.createRange() const newRange = document.createRange()
newRange.selectNodeContents(contentSpan) newRange.selectNodeContents(contentSpan)
newRange.collapse(false) newRange.collapse(false)
selection.removeAllRanges() selection.removeAllRanges()
selection.addRange(newRange) selection.addRange(newRange)
// 添加事件监听器到内容区域,监听内容变化
const checkContent = () => {
// 延迟检查,确保内容已更新
setTimeout(() => {
if (contentSpan.textContent.trim() === '') {
// 如果内容为空,移除整个引用容器
quoteContainer.remove()
handleInput()
}
}, 0)
}
contentSpan.addEventListener('input', checkContent)
contentSpan.addEventListener('blur', checkContent)
handleInput() handleInput()
} }
} }
@@ -129,6 +207,9 @@ const insertTodoList = () => {
if (selection.rangeCount > 0) { if (selection.rangeCount > 0) {
const range = selection.getRangeAt(0) const range = selection.getRangeAt(0)
// 检查嵌套限制
if (isInListOrQuote()) return
// 创建待办事项容器 // 创建待办事项容器
const todoContainer = document.createElement('div') const todoContainer = document.createElement('div')
todoContainer.contentEditable = false todoContainer.contentEditable = false
@@ -179,7 +260,7 @@ const insertTodoList = () => {
handleInput() handleInput()
}) })
// 添加事件监听器到内容区域,监听内容变化 // 添加事件监听器到内容区域,监听内容变化和按键事件
const checkContent = () => { const checkContent = () => {
// 延迟检查,确保内容已更新 // 延迟检查,确保内容已更新
setTimeout(() => { setTimeout(() => {
@@ -194,6 +275,150 @@ const insertTodoList = () => {
contentSpan.addEventListener('input', checkContent) contentSpan.addEventListener('input', checkContent)
contentSpan.addEventListener('blur', checkContent) contentSpan.addEventListener('blur', checkContent)
// 监听回车键,创建同级待办事项
contentSpan.addEventListener('keydown', e => {
if (e.key === 'Enter') {
e.preventDefault()
// 创建新的待办事项
const newTodoContainer = document.createElement('div')
newTodoContainer.contentEditable = false
newTodoContainer.className = 'todo-container'
// 创建图标元素
const newIcon = document.createElement('img')
newIcon.className = 'todo-icon'
newIcon.src = '/assets/icons/drawable-xxhdpi/rtf_icon_gtasks.png'
newIcon.alt = '待办事项'
// 创建内容容器
const newContentSpan = document.createElement('span')
newContentSpan.contentEditable = true
newContentSpan.className = 'todo-content'
newContentSpan.textContent = ''
// 组装元素
newTodoContainer.appendChild(newIcon)
newTodoContainer.appendChild(newContentSpan)
// 插入到当前待办事项后面
todoContainer.parentNode.insertBefore(newTodoContainer, todoContainer.nextSibling)
// 添加换行
const newBr = document.createElement('br')
newTodoContainer.parentNode.insertBefore(newBr, newTodoContainer.nextSibling)
// 聚焦到新内容区域
const newRange = document.createRange()
newRange.selectNodeContents(newContentSpan)
newRange.collapse(false)
selection.removeAllRanges()
selection.addRange(newRange)
// 添加事件监听器到新图标
newIcon.addEventListener('click', function () {
// 根据当前状态切换图标
if (this.src.includes('rtf_icon_gtasks.png')) {
this.src = '/assets/icons/drawable-xxhdpi/rtf_icon_gtasks_light.png'
newContentSpan.style.color = 'var(--text-tertiary)'
newContentSpan.style.textDecoration = 'line-through'
} else {
this.src = '/assets/icons/drawable-xxhdpi/rtf_icon_gtasks.png'
newContentSpan.style.color = 'var(--note-content)'
newContentSpan.style.textDecoration = 'none'
}
handleInput()
})
// 添加事件监听器到新内容区域
const newCheckContent = () => {
setTimeout(() => {
if (newContentSpan.textContent.trim() === '') {
newTodoContainer.remove()
handleInput()
}
}, 0)
}
newContentSpan.addEventListener('input', newCheckContent)
newContentSpan.addEventListener('blur', newCheckContent)
// 监听新内容区域的回车键
newContentSpan.addEventListener('keydown', e => {
if (e.key === 'Enter') {
e.preventDefault()
// 创建新的待办事项
const nextTodoContainer = document.createElement('div')
nextTodoContainer.contentEditable = false
nextTodoContainer.className = 'todo-container'
// 创建图标元素
const nextIcon = document.createElement('img')
nextIcon.className = 'todo-icon'
nextIcon.src = '/assets/icons/drawable-xxhdpi/rtf_icon_gtasks.png'
nextIcon.alt = '待办事项'
// 创建内容容器
const nextContentSpan = document.createElement('span')
nextContentSpan.contentEditable = true
nextContentSpan.className = 'todo-content'
nextContentSpan.textContent = ''
// 组装元素
nextTodoContainer.appendChild(nextIcon)
nextTodoContainer.appendChild(nextContentSpan)
// 插入到当前待办事项后面
newTodoContainer.parentNode.insertBefore(nextTodoContainer, newTodoContainer.nextSibling)
// 添加换行
const nextBr = document.createElement('br')
nextTodoContainer.parentNode.insertBefore(nextBr, nextTodoContainer.nextSibling)
// 聚焦到新内容区域
const nextRange = document.createRange()
nextRange.selectNodeContents(nextContentSpan)
nextRange.collapse(false)
selection.removeAllRanges()
selection.addRange(nextRange)
// 添加事件监听器到新图标
nextIcon.addEventListener('click', function () {
// 根据当前状态切换图标
if (this.src.includes('rtf_icon_gtasks.png')) {
this.src = '/assets/icons/drawable-xxhdpi/rtf_icon_gtasks_light.png'
nextContentSpan.style.color = 'var(--text-tertiary)'
nextContentSpan.style.textDecoration = 'line-through'
} else {
this.src = '/assets/icons/drawable-xxhdpi/rtf_icon_gtasks.png'
nextContentSpan.style.color = 'var(--note-content)'
nextContentSpan.style.textDecoration = 'none'
}
handleInput()
})
// 添加事件监听器到新内容区域
const nextCheckContent = () => {
setTimeout(() => {
if (nextContentSpan.textContent.trim() === '') {
nextTodoContainer.remove()
handleInput()
}
}, 0)
}
nextContentSpan.addEventListener('input', nextCheckContent)
nextContentSpan.addEventListener('blur', nextCheckContent)
handleInput()
}
})
handleInput()
}
})
handleInput() handleInput()
} }
} }
@@ -364,18 +589,18 @@ defineExpose({
} }
/* 自定义内容样式 */ /* 自定义内容样式 */
.editor-content h2 { :deep(.editor-content h2) {
font-size: 20px; font-size: 20px;
font-weight: 600; font-weight: 600;
margin: 16px 0 8px 0; margin: 8px 0 8px 0;
color: var(--note-title); color: var(--note-title);
line-height: 1.4; line-height: 1.4;
letter-spacing: 0.5px; letter-spacing: 0.5px;
padding-left: 24px; text-align: center;
position: relative; position: relative;
} }
.editor-content blockquote { :deep(.editor-content blockquote) {
border-left: 3px solid var(--primary); border-left: 3px solid var(--primary);
padding-left: 16px; padding-left: 16px;
margin: 16px 0; margin: 16px 0;
@@ -386,12 +611,12 @@ defineExpose({
font-style: italic; font-style: italic;
} }
.quote-container { :deep(.quote-container) {
position: relative; position: relative;
margin: 16px 0; margin: 16px 0;
} }
.quote-icon { :deep(.quote-icon) {
position: absolute; position: absolute;
left: 0; left: 0;
top: 10px; top: 10px;
@@ -399,7 +624,7 @@ defineExpose({
height: 16px; height: 16px;
} }
.quote-content { :deep(.quote-content) {
border-left: 3px solid var(--primary); border-left: 3px solid var(--primary);
padding-left: 32px; padding-left: 32px;
margin-left: 16px; margin-left: 16px;
@@ -410,40 +635,40 @@ defineExpose({
font-style: italic; font-style: italic;
} }
.editor-content ul, :deep(.editor-content ul),
.editor-content ol { :deep(.editor-content ol) {
margin: 16px 0; margin: 16px 0;
padding-left: 32px; padding-left: 32px;
position: relative; position: relative;
} }
.editor-content li { :deep(.editor-content li) {
margin: 6px 0; margin: 6px 0;
line-height: 1.5; line-height: 1.5;
} }
.editor-content p { :deep(.editor-content p) {
margin: 0 0 12px 0; margin: 0 0 12px 0;
line-height: 1.6; line-height: 1.6;
letter-spacing: 0.3px; letter-spacing: 0.3px;
} }
.editor-content strong { :deep(.editor-content strong) {
color: var(--text-primary); color: var(--text-primary);
font-weight: 600; font-weight: 600;
} }
.editor-content em { :deep(.editor-content em) {
color: var(--text-secondary); color: var(--text-secondary);
font-style: italic; font-style: italic;
} }
.editor-content u { :deep(.editor-content u) {
text-decoration: none; text-decoration: none;
border-bottom: 1px solid var(--text-primary); border-bottom: 1px solid var(--text-primary);
} }
.editor-content hr { :deep(.editor-content hr) {
border: none; border: none;
height: 1px; height: 1px;
background-color: var(--border); background-color: var(--border);
@@ -458,16 +683,16 @@ defineExpose({
:deep(.todo-container) { :deep(.todo-container) {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
margin: 0.5rem 0; margin: 0.1rem 0;
padding: 0.25rem 0; padding: 0.02rem 0;
position: relative; position: relative;
padding-left: 1.5rem; padding-left: 1.2rem;
} }
:deep(.todo-icon) { :deep(.todo-icon) {
position: absolute; position: absolute;
left: 0; left: 0;
top: 0.2rem; top: 0;
width: 2rem; width: 2rem;
height: 2rem; height: 2rem;
cursor: pointer; cursor: pointer;
@@ -507,57 +732,4 @@ defineExpose({
border-radius: 0 0.25rem 0.25rem 0; border-radius: 0 0.25rem 0.25rem 0;
font-style: italic; font-style: italic;
} }
/* 格式标识图标 */
.editor-content h2::before {
content: '';
display: inline-block;
width: 16px;
height: 16px;
background-image: url('/assets/icons/drawable-xxhdpi/rtf_header_normal.9.png');
background-size: contain;
background-repeat: no-repeat;
position: absolute;
left: 0;
top: 2px;
}
.editor-content blockquote::before {
content: '';
display: inline-block;
width: 16px;
height: 16px;
background-image: url('/assets/icons/drawable-xxhdpi/rtf_quot_normal.9.png');
background-size: contain;
background-repeat: no-repeat;
position: absolute;
left: 8px;
top: 10px;
}
.editor-content ul::before {
content: '';
display: inline-block;
width: 16px;
height: 16px;
background-image: url('/assets/icons/drawable-xxhdpi/rtf_list_normal.9.png');
background-size: contain;
background-repeat: no-repeat;
position: absolute;
left: 0;
top: 2px;
}
.editor-content ol::before {
content: '';
display: inline-block;
width: 16px;
height: 16px;
background-image: url('/assets/icons/drawable-xxhdpi/rtf_list_normal.9.png');
background-size: contain;
background-repeat: no-repeat;
position: absolute;
left: 0;
top: 2px;
}
</style> </style>