You've already forked SmartisanNote.Remake
优化了编辑器的格式
This commit is contained in:
@@ -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()
|
||||||
@@ -86,6 +138,17 @@ const insertQuote = () => {
|
|||||||
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'
|
||||||
@@ -119,6 +182,21 @@ const insertQuote = () => {
|
|||||||
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>
|
||||||
|
|||||||
Reference in New Issue
Block a user