Files
SmartisanNote.Remake/src/components/RichTextEditor.vue

2624 lines
96 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="editor-container">
<!-- 工具栏 -->
<div class="toolbar" :class="{ visible: isToolbarVisible, 'keyboard-visible': isKeyboardVisible, 'dynamic-position': isKeyboardVisible }" @mousedown.prevent @focusin="keepToolbarVisible" @focusout="handleToolbarFocusOut">
<button v-for="tool in tools" :key="tool.name" :class="{ active: tool.active }" @click.stop="handleToolClick(tool.action, $event)" @mousedown.prevent @focusout.prevent class="toolbar-btn">
<img :src="tool.icon" :alt="tool.name" class="toolbar-icon" />
</button>
</div>
<!-- 编辑区域 -->
<div ref="editorRef" contenteditable="true" class="editor-content" @input="handleInput" @keydown="handleKeydown" @click="updateToolbarState" @keyup="updateToolbarState" @focus="showToolbar" @blur="handleBlur"></div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, nextTick, watch } from 'vue'
const props = defineProps({
modelValue: {
type: String,
default: '',
},
})
const emit = defineEmits(['update:modelValue'])
const editorRef = ref(null)
const content = ref(props.modelValue || '')
const isToolbarVisible = ref(false)
const isKeyboardVisible = ref(false)
const initialViewportHeight = ref(0)
const dragState = ref({
isDragging: false,
draggedImage: null,
startX: 0,
startY: 0,
currentY: 0,
longPressTimer: null,
isLongPress: false,
indicator: null,
lastCheckTime: 0,
lastMoveTime: 0
})
// 初始化编辑器内容
onMounted(() => {
console.log('Editor mounted')
if (editorRef.value) {
console.log('Editor ref available')
if (props.modelValue) {
console.log('Setting initial content')
try {
editorRef.value.innerHTML = props.modelValue
content.value = props.modelValue
console.log('Initial content set successfully')
// 调整已有图片的高度
adjustExistingImages()
} catch (error) {
console.error('Failed to set initial content:', error)
}
} else {
// 即使没有初始内容,也要确保编辑器是可编辑的
editorRef.value.contentEditable = true
console.log('Editor initialized without initial content')
}
}
// 记录初始视口高度
initialViewportHeight.value = window.visualViewport?.height || window.innerHeight
console.log('Initial viewport height:', initialViewportHeight.value)
// 初始化CSS变量
document.documentElement.style.setProperty('--viewport-height', `${initialViewportHeight.value}px`)
console.log('Set viewport height CSS variable')
// 添加虚拟键盘检测事件监听器
if (window.visualViewport) {
console.log('Adding viewport resize listener')
window.visualViewport.addEventListener('resize', handleViewportResize)
} else {
console.log('Adding window resize listener')
window.addEventListener('resize', handleWindowResize)
}
// 为已有图片添加拖拽事件监听器
setTimeout(() => {
console.log('Adding drag event listeners to existing images')
const imageContainers = editorRef.value.querySelectorAll('.image-container')
console.log('Found existing image containers:', imageContainers.length)
imageContainers.forEach(container => {
const img = container.querySelector('img.editor-image')
if (!img) return
console.log('Adding touch listeners to image:', img)
// 添加触摸事件监听器
container.addEventListener('touchstart', handleTouchStart)
container.addEventListener('touchmove', handleTouchMove)
container.addEventListener('touchend', handleTouchEnd)
container.addEventListener('touchcancel', handleTouchCancel)
// 为删除按钮添加点击事件
let deleteBtn = container.querySelector('.image-delete-btn')
if (!deleteBtn) {
// 如果删除按钮不存在,创建它
console.log('Delete button not found in mounted hook, creating new one')
deleteBtn = document.createElement('div')
deleteBtn.className = 'image-delete-btn'
deleteBtn.style.cssText = 'position: absolute; top: 8px; right: 8px; width: 24px; height: 24px; cursor: pointer; z-index: 1000; display: none; transition: opacity 0.2s ease; touch-action: manipulation;'
container.appendChild(deleteBtn)
}
if (deleteBtn) {
// 先移除可能已有的事件监听器,避免重复
deleteBtn.removeEventListener('click', null)
deleteBtn.removeEventListener('touchend', null)
deleteBtn.addEventListener('click', function(e) {
e.stopPropagation();
container.remove();
handleInput();
});
deleteBtn.addEventListener('touchend', function(e) {
e.stopPropagation();
container.remove();
handleInput();
});
}
// 为图片容器添加短按事件以显示/隐藏删除按钮
// 先移除可能已有的事件监听器,避免重复
const touchStartHandler = container._touchStartHandler;
const touchEndHandler = container._touchEndHandler;
if (touchStartHandler) {
container.removeEventListener('touchstart', touchStartHandler);
}
if (touchEndHandler) {
container.removeEventListener('touchend', touchEndHandler);
}
let touchStartTime = 0;
const newTouchStartHandler = function(e) {
touchStartTime = Date.now();
};
const newTouchEndHandler = function(e) {
const touchDuration = Date.now() - touchStartTime;
console.log('Touch end event triggered in mounted hook, duration:', touchDuration, 'isLongPress:', dragState.value.isLongPress);
// 短按小于200ms且非长按拖拽状态时切换删除按钮显示
if (touchDuration < 200 && !dragState.value.isLongPress) {
e.stopPropagation();
console.log('Short tap detected in mounted hook, toggling delete button visibility');
// 切换删除按钮的显示状态
if (deleteBtn) {
console.log('Current delete button display style in mounted hook:', deleteBtn.style.display);
// 检查删除按钮当前是否可见
const computedStyle = getComputedStyle(deleteBtn);
const isCurrentlyVisible = deleteBtn.style.display === 'block' ||
computedStyle.display === 'block' ||
(deleteBtn.style.display !== 'none' &&
computedStyle.display !== 'none');
console.log('Delete button current styles in mounted hook - inline:', deleteBtn.style.display, 'computed:', computedStyle.display);
if (isCurrentlyVisible) {
deleteBtn.style.display = 'none';
console.log('Delete button hidden in mounted hook');
} else {
deleteBtn.style.display = 'block';
console.log('Delete button displayed in mounted hook');
}
} else {
console.log('Delete button not found in mounted hook');
}
} else {
console.log('Not a short tap or isLongPress is true in mounted hook');
}
};
container.addEventListener('touchstart', newTouchStartHandler);
container.addEventListener('touchend', newTouchEndHandler);
// 保存事件处理函数的引用,以便后续移除
container._touchStartHandler = newTouchStartHandler;
container._touchEndHandler = newTouchEndHandler;
})
}, 0)
})
// 组件卸载时移除事件监听器
onUnmounted(() => {
if (window.visualViewport) {
window.visualViewport.removeEventListener('resize', handleViewportResize)
} else {
window.removeEventListener('resize', handleWindowResize)
}
// 清理所有图片容器的事件监听器
if (editorRef.value) {
const imageContainers = editorRef.value.querySelectorAll('.image-container')
imageContainers.forEach(container => {
// 移除拖拽事件监听器
container.removeEventListener('touchstart', handleTouchStart)
container.removeEventListener('touchmove', handleTouchMove)
container.removeEventListener('touchend', handleTouchEnd)
container.removeEventListener('touchcancel', handleTouchCancel)
// 移除短按事件监听器
const touchStartHandler = container._touchStartHandler;
const touchEndHandler = container._touchEndHandler;
if (touchStartHandler) {
container.removeEventListener('touchstart', touchStartHandler);
delete container._touchStartHandler;
}
if (touchEndHandler) {
container.removeEventListener('touchend', touchEndHandler);
delete container._touchEndHandler;
}
// 移除删除按钮事件监听器
const deleteBtn = container.querySelector('.image-delete-btn')
if (deleteBtn) {
deleteBtn.removeEventListener('click', null)
deleteBtn.removeEventListener('touchend', null)
}
})
}
})
// 工具栏配置
// 定义富文本编辑器的所有工具按钮及其功能
const tools = ref([
{
name: 'bold', // 加粗工具
icon: '/assets/icons/drawable-xxhdpi/rtf_bold_normal.9.png',
action: () => formatText('bold'), // 执行加粗格式化
active: false, // 工具是否处于激活状态
},
{
name: 'center', // 居中对齐工具
icon: '/assets/icons/drawable-xxhdpi/rtf_center_normal.9.png',
action: () => formatText('justifyCenter'), // 执行居中对齐格式化
active: false,
},
{
name: 'todo', // 待办事项工具
icon: '/assets/icons/drawable-xxhdpi/rtf_gtasks_normal.9.png',
action: () => formatText('insertTodoList'), // 插入待办事项列表
active: false,
},
{
name: 'list', // 无序列表工具
icon: '/assets/icons/drawable-xxhdpi/rtf_list_normal.9.png',
action: () => formatText('insertUnorderedList'), // 插入无序列表
active: false,
},
{
name: 'header', // 标题工具
icon: '/assets/icons/drawable-xxhdpi/rtf_header_normal.9.png',
action: () => formatText('formatBlock', 'h2'), // 格式化为二级标题
active: false,
},
{
name: 'quote', // 引用工具
icon: '/assets/icons/drawable-xxhdpi/rtf_quot_normal.9.png',
action: () => insertQuote(), // 插入引用格式
active: false,
},
])
// 处理输入事件
const handleInput = () => {
if (editorRef.value) {
// 获取编辑器内容(不清理,保持功能完整)
let innerHTML = editorRef.value.innerHTML;
// 处理换行符,确保在段落之间有明确的分隔
innerHTML = innerHTML.replace(/<\/p><p>/g, '</p>\n<p>')
// 处理div标签换行
innerHTML = innerHTML.replace(/<\/div><div>/g, '</div>\n<div>')
// 处理br标签换行
innerHTML = innerHTML.replace(/<br>/g, '\n')
innerHTML = innerHTML.replace(/<br\/>/g, '\n')
// 处理div标签内的换行
innerHTML = innerHTML.replace(/<div>/g, '\n<div>')
content.value = innerHTML
emit('update:modelValue', content.value)
}
}
// 检查当前选区是否已经在某种格式中
// 用于防止重复应用相同的格式,例如重复加粗
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) => {
// 检查是否已经应用了相同的格式,如果已应用则取消格式
// 例如,如果文本已经是加粗的,再次点击加粗按钮会取消加粗
if (command === 'bold' && isAlreadyInFormat('bold')) {
document.execCommand('bold', false, null)
updateToolbarState()
handleInput()
return
}
// 处理居中对齐切换:如果已居中则取消居中
if (command === 'justifyCenter' && isAlreadyInFormat('center')) {
document.execCommand('justifyLeft', false, null)
updateToolbarState()
handleInput()
return
}
// 处理标题格式切换:如果已是标题则转为普通段落
if (command === 'formatBlock' && value === 'h2' && isAlreadyInFormat('header')) {
document.execCommand('formatBlock', false, '<p>')
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()) {
return
}
// 执行格式化命令
document.execCommand(command, false, value)
// 更新工具栏状态以反映当前格式
updateToolbarState()
// 触发输入事件以更新内容
handleInput()
}
// 插入水平线
const insertHorizontalRule = () => {
document.execCommand('insertHorizontalRule', false, null)
handleInput()
}
// 插入引用格式
const insertQuote = () => {
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 === 'BLOCKQUOTE' || (current.classList && current.classList.contains('quote-content'))) return
current = current.parentElement
}
// 检查嵌套限制
if (isInListOrQuote()) return
// 创建引用容器
const quoteContainer = document.createElement('div')
quoteContainer.className = 'quote-container'
// 创建图标元素
const icon = document.createElement('img')
icon.className = 'quote-icon'
icon.src = '/assets/icons/drawable-xxhdpi/rag_quote.png'
icon.alt = '引用'
// 创建内容容器
const contentSpan = document.createElement('div')
contentSpan.className = 'quote-content'
contentSpan.textContent = '引用内容'
// 组装元素
quoteContainer.appendChild(icon)
quoteContainer.appendChild(contentSpan)
// 插入到当前光标位置
range.insertNode(quoteContainer)
// 添加换行
const br = document.createElement('br')
quoteContainer.parentNode.insertBefore(br, quoteContainer.nextSibling)
// 聚焦到内容区域延迟执行确保在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 = () => {
// 延迟检查,确保内容已更新
setTimeout(() => {
if (contentSpan.textContent.trim() === '') {
// 如果内容为空,移除整个引用容器
quoteContainer.remove()
handleInput()
}
}, 0)
}
contentSpan.addEventListener('input', checkContent)
contentSpan.addEventListener('blur', checkContent)
handleInput()
}
}
// 插入待办事项列表
// 创建一个可交互的待办事项元素,包含复选框图标和可编辑内容区域
const insertTodoList = () => {
const selection = window.getSelection()
if (selection.rangeCount > 0) {
const range = selection.getRangeAt(0)
// 检查嵌套限制,防止在列表或引用中插入待办事项
if (isInListOrQuote()) return
// 创建待办事项容器
const todoContainer = document.createElement('div')
todoContainer.contentEditable = false // 容器本身不可编辑
todoContainer.className = 'todo-container'
// 创建图标元素(复选框)
const icon = document.createElement('img')
icon.className = 'todo-icon'
icon.src = '/assets/icons/drawable-xxhdpi/rtf_icon_gtasks.png' // 未完成状态图标
icon.alt = '待办事项'
// 创建内容容器(可编辑区域)
const contentSpan = document.createElement('div')
contentSpan.contentEditable = true // 内容区域可编辑
contentSpan.className = 'todo-content'
contentSpan.textContent = '待办事项' // 默认文本
// 组装元素:将图标和内容区域添加到容器中
todoContainer.appendChild(icon)
todoContainer.appendChild(contentSpan)
// 插入到当前光标位置
range.insertNode(todoContainer)
// 添加换行,确保待办事项下方有空白行
const br = document.createElement('br')
todoContainer.parentNode.insertBefore(br, todoContainer.nextSibling)
// 聚焦到内容区域延迟执行确保在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 () {
// 根据当前状态切换图标和样式
if (this.src.includes('rtf_icon_gtasks.png')) {
// 切换到完成状态
this.src = '/assets/icons/drawable-xxhdpi/rtf_icon_gtasks_light.png' // 完成状态图标
contentSpan.style.color = 'var(--text-tertiary)' // 灰色文字
contentSpan.style.textDecoration = 'line-through' // 添加删除线
} else {
// 切换到未完成状态
this.src = '/assets/icons/drawable-xxhdpi/rtf_icon_gtasks.png' // 未完成状态图标
contentSpan.style.color = 'var(--note-content)' // 正常文字颜色
contentSpan.style.textDecoration = 'none' // 移除删除线
}
handleInput() // 触发内容更新
})
// 添加事件监听器到内容区域,监听内容变化和按键事件
const checkContent = () => {
// 延迟检查,确保内容已更新
setTimeout(() => {
if (contentSpan.textContent.trim() === '') {
// 如果内容为空,移除整个待办事项容器
todoContainer.remove()
handleInput()
}
}, 0)
}
contentSpan.addEventListener('input', checkContent) // 内容输入时检查
contentSpan.addEventListener('blur', checkContent) // 失去焦点时检查
// 添加焦点事件监听器,确保工具栏在待办事项获得焦点时保持可见
contentSpan.addEventListener('focus', () => {
isToolbarVisible.value = true
})
// 监听回车键,创建同级待办事项
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('div')
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('focus', () => {
isToolbarVisible.value = true
})
// 监听新内容区域的回车键
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('div')
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)
// 添加焦点事件监听器,确保工具栏在待办事项获得焦点时保持可见
nextContentSpan.addEventListener('focus', () => {
isToolbarVisible.value = true
})
handleInput()
}
})
handleInput()
}
})
handleInput()
}
}
// 重置拖拽状态
const resetDragState = () => {
// 清除长按定时器
if (dragState.value.longPressTimer) {
clearTimeout(dragState.value.longPressTimer)
dragState.value.longPressTimer = null
}
// 重置所有拖拽状态
dragState.value.isLongPress = false
dragState.value.draggedImage = null
dragState.value.startX = 0
dragState.value.startY = 0
dragState.value.currentY = 0
dragState.value.lastMoveTime = 0
// 移除拖拽指示器
if (dragState.value.indicator) {
const indicator = dragState.value.indicator
indicator.style.opacity = '0'
setTimeout(() => {
if (indicator.parentNode) {
indicator.parentNode.removeChild(indicator)
}
}, 150)
dragState.value.indicator = null
}
// 重置所有图片的拖拽样式
if (editorRef.value) {
const draggedImages = editorRef.value.querySelectorAll('.editor-image.dragging')
draggedImages.forEach(img => {
img.classList.remove('dragging')
img.style.zIndex = ''
img.style.transition = ''
img.style.transform = ''
img.style.opacity = ''
})
}
}
// 插入图片
const insertImage = () => {
console.log('Inserting image')
// 创建文件输入元素
const fileInput = document.createElement('input')
fileInput.type = 'file'
fileInput.accept = 'image/*'
fileInput.style.display = 'none'
// 添加到文档中
document.body.appendChild(fileInput)
// 监听文件选择事件
fileInput.addEventListener('change', function (event) {
console.log('File selected:', event.target.files)
const file = event.target.files[0]
if (file && file.type.startsWith('image/')) {
console.log('Image file selected')
// 创建FileReader读取文件
const reader = new FileReader()
reader.onload = function (e) {
console.log('File read successfully')
// 获取图片数据URL
const imageDataUrl = e.target.result
console.log('Image data URL:', imageDataUrl)
// 获取当前选区
const selection = window.getSelection()
if (selection.rangeCount > 0) {
let range = selection.getRangeAt(0)
console.log('Current range:', range)
// 检查选区是否在图片容器内部,如果是则调整到容器后面
const startContainer = range.startContainer
let imageContainer = null
// 如果startContainer是图片容器本身
if (startContainer.classList && startContainer.classList.contains('image-container')) {
imageContainer = startContainer
}
// 如果startContainer是图片容器的子元素
else if (startContainer.parentNode && startContainer.parentNode.classList &&
startContainer.parentNode.classList.contains('image-container')) {
imageContainer = startContainer.parentNode
}
// 向上查找父元素
else {
let parent = startContainer.parentNode
while (parent && parent !== editorRef.value) {
if (parent.classList && parent.classList.contains('image-container')) {
imageContainer = parent
break
}
parent = parent.parentNode
}
}
// 如果选区在图片容器内部,调整到容器后面
if (imageContainer) {
console.log('Selection is inside image container, adjusting range')
range = document.createRange()
range.setStartAfter(imageContainer)
range.collapse(true)
}
// 创建图片容器
const imgContainer = document.createElement('div')
imgContainer.className = 'image-container'
imgContainer.style.position = 'relative'
imgContainer.style.display = 'inline-block'
// 创建图片元素
const img = document.createElement('img')
img.src = imageDataUrl
img.className = 'editor-image'
img.setAttribute('data-draggable', 'true')
img.style.maxWidth = '100%'
img.style.height = 'auto'
img.style.display = 'block'
img.style.objectFit = 'cover'
img.style.boxSizing = 'border-box'
img.style.border = '0.625rem solid white'
img.style.borderRadius = '0.2rem'
img.style.boxShadow = '0 1px 5px rgba(0, 0, 0, 0.18)'
img.style.background = 'var(--background-secondary)'
img.style.position = 'relative'
img.style.outline = 'none' // 移除默认焦点轮廓
img.style.userSelect = 'none' // 防止选中
img.style.webkitUserSelect = 'none' // 防止选中
img.style.mozUserSelect = 'none' // 防止选中
img.style.msUserSelect = 'none' // 防止选中
img.style.webkitTouchCallout = 'none' // 防止长按弹出菜单
img.style.webkitTapHighlightColor = 'transparent' // 防止点击高亮
img.draggable = true
// 创建删除按钮
const deleteBtn = document.createElement('div')
deleteBtn.className = 'image-delete-btn'
deleteBtn.style.cssText = 'position: absolute; top: 8px; right: 8px; width: 24px; height: 24px; cursor: pointer; z-index: 1000; display: none; transition: opacity 0.2s ease; touch-action: manipulation;'
// 将图片和删除按钮添加到容器中
imgContainer.appendChild(img)
imgContainer.appendChild(deleteBtn)
console.log('Created image element:', img)
// 创建一个临时图片来获取原始尺寸
const tempImg = new Image()
tempImg.onload = function () {
console.log('Temp image loaded')
// 获取CSS变量
const editorFontSize = getComputedStyle(document.documentElement).getPropertyValue('--editor-font-size').trim() || '16px'
const editorLineHeight = getComputedStyle(document.documentElement).getPropertyValue('--editor-line-height').trim() || '1.6'
const fontSize = parseInt(editorFontSize)
const lineHeight = parseFloat(editorLineHeight)
// 计算行高
const computedLineHeight = fontSize * lineHeight
// 获取编辑器的宽度(减去一些内边距)
const editorWidth = editorRef.value.offsetWidth - 20 // 20px为左右内边距
// 按宽度撑满计算调整后的尺寸
const originalHeight = tempImg.height
const originalWidth = tempImg.width
const scaleRatio = editorWidth / originalWidth
const scaledHeight = originalHeight * scaleRatio
// 计算调整后的高度,使其为行高的整数倍
const scaleFactor = Math.max(1, Math.round(scaledHeight / computedLineHeight))
const adjustedHeight = scaleFactor * computedLineHeight
// 按比例调整宽度
const adjustedWidth = (originalWidth * adjustedHeight) / originalHeight
img.style.height = `${adjustedHeight}px`
img.style.width = `${adjustedWidth}px`
// 确保图片与基准线对齐
img.style.verticalAlign = 'top'
}
tempImg.src = imageDataUrl
// 添加触摸事件监听器实现拖拽功能
// 先移除可能已有的事件监听器,避免重复
imgContainer.removeEventListener('touchstart', handleTouchStart)
imgContainer.removeEventListener('touchmove', handleTouchMove)
imgContainer.removeEventListener('touchend', handleTouchEnd)
imgContainer.removeEventListener('touchcancel', handleTouchCancel)
// 重新添加事件监听器
imgContainer.addEventListener('touchstart', handleTouchStart)
imgContainer.addEventListener('touchmove', handleTouchMove)
imgContainer.addEventListener('touchend', handleTouchEnd)
imgContainer.addEventListener('touchcancel', handleTouchCancel)
// 为删除按钮添加点击事件(鼠标和触摸)
// 先移除可能已有的事件监听器,避免重复
deleteBtn.removeEventListener('click', null)
deleteBtn.removeEventListener('touchend', null)
deleteBtn.addEventListener('click', function(e) {
e.stopPropagation();
imgContainer.remove();
handleInput();
});
deleteBtn.addEventListener('touchend', function(e) {
e.stopPropagation();
imgContainer.remove();
handleInput();
});
// 为图片容器添加短按事件以显示/隐藏删除按钮
let touchStartTime = 0;
const touchStartHandler = function(e) {
touchStartTime = Date.now();
};
const touchEndHandler = function(e) {
const touchDuration = Date.now() - touchStartTime;
console.log('Touch end event triggered, duration:', touchDuration, 'isLongPress:', dragState.value.isLongPress);
// 短按小于200ms且非长按拖拽状态时切换删除按钮显示
if (touchDuration < 200 && !dragState.value.isLongPress) {
e.stopPropagation();
console.log('Short tap detected, toggling delete button visibility');
// 切换删除按钮的显示状态
if (deleteBtn) {
console.log('Current delete button display style:', deleteBtn.style.display);
// 检查删除按钮当前是否可见
const computedStyle = getComputedStyle(deleteBtn);
const isCurrentlyVisible = deleteBtn.style.display === 'block' ||
computedStyle.display === 'block' ||
(deleteBtn.style.display !== 'none' &&
computedStyle.display !== 'none');
console.log('Delete button current styles - inline:', deleteBtn.style.display, 'computed:', computedStyle.display);
console.log('Delete button background image:', computedStyle.backgroundImage);
console.log('Delete button width:', computedStyle.width, 'height:', computedStyle.height);
console.log('Delete button position:', computedStyle.position);
console.log('Delete button z-index:', computedStyle.zIndex);
if (isCurrentlyVisible) {
deleteBtn.style.display = 'none';
console.log('Delete button hidden');
} else {
deleteBtn.style.display = 'block';
console.log('Delete button displayed');
// 添加调试样式以确保可见
deleteBtn.style.backgroundColor = 'rgba(255, 0, 0, 0.3)'; // 半透明红色背景用于调试
}
} else {
console.log('Delete button not found');
}
} else {
console.log('Not a short tap or isLongPress is true');
}
};
imgContainer.addEventListener('touchstart', touchStartHandler);
imgContainer.addEventListener('touchend', touchEndHandler);
// 保存事件处理函数的引用,以便后续移除
imgContainer._touchStartHandler = touchStartHandler;
imgContainer._touchEndHandler = touchEndHandler;
console.log('Added touch event listeners')
// 插入图片容器到当前光标位置
range.insertNode(imgContainer)
console.log('Inserted image container into editor')
// 调试信息
console.log('Image container inserted:', imgContainer)
console.log('Next sibling (should be drag handle):', imgContainer.nextSibling)
// 添加换行
const br = document.createElement('br')
imgContainer.parentNode.insertBefore(br, imgContainer.nextSibling)
console.log('Added line break after image container')
// 修正选区位置,避免嵌套插入
// 使用setTimeout确保DOM更新完成后再设置选区
setTimeout(() => {
const newRange = document.createRange();
newRange.setStartAfter(br);
newRange.collapse(true);
const sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(newRange);
// 重新聚焦到编辑器
if (editorRef.value) {
editorRef.value.focus()
console.log('Focused editor')
}
}, 0);
// 触发输入事件更新内容
handleInput()
console.log('Handled input event')
}
}
reader.readAsDataURL(file)
}
// 清理文件输入元素
document.body.removeChild(fileInput)
console.log('Removed file input')
})
// 触发文件选择对话框
fileInput.click()
console.log('Clicked file input')
}
// 处理键盘事件
const handleKeydown = e => {
// 处理Shift+Enter键换行
if (e.key === 'Enter' && e.shiftKey) {
e.preventDefault()
document.execCommand('insertHTML', false, '<br><br>')
handleInput()
return
}
// 处理Tab键缩进
if (e.key === 'Tab') {
e.preventDefault()
document.execCommand('insertHTML', false, '    ')
handleInput()
return
}
// 处理回车键创建新列表项
if (e.key === 'Enter' && !e.shiftKey) {
// 检查是否在列表中
const selection = window.getSelection()
if (selection.rangeCount > 0) {
const range = selection.getRangeAt(0)
const container = range.startContainer
// 检查是否在ul或ol内
let listElement = null
let current = container.nodeType === Node.TEXT_NODE ? container.parentElement : container
while (current && current !== editorRef.value) {
if (current.tagName === 'LI') {
listElement = current
break
}
current = current.parentElement
}
// 如果在列表项中且为空,退出列表
if (listElement) {
const liContent = listElement.textContent.trim()
if (liContent === '') {
e.preventDefault()
// 退出列表
document.execCommand('outdent')
handleInput()
}
}
}
}
}
// 处理触摸开始事件
const handleTouchStart = (e) => {
const img = e.target
if (!img.classList.contains('editor-image')) return
// 防止图片被选中 - 在触摸开始时就应用
img.style.userSelect = 'none'
img.style.webkitUserSelect = 'none'
img.style.mozUserSelect = 'none'
img.style.msUserSelect = 'none'
img.style.webkitTouchCallout = 'none'
img.style.webkitTapHighlightColor = 'transparent'
// 清除之前的定时器
if (dragState.value.longPressTimer) {
clearTimeout(dragState.value.longPressTimer)
}
// 记录触摸开始位置
dragState.value.startX = e.touches[0].clientX
dragState.value.startY = e.touches[0].clientY
// 设置长按检测定时器500毫秒
dragState.value.longPressTimer = setTimeout(() => {
dragState.value.isLongPress = true
dragState.value.draggedImage = img
dragState.value.startY = e.touches[0].clientY
dragState.value.currentY = e.touches[0].clientY
// 添加拖拽样式
img.classList.add('dragging')
img.style.opacity = '0.85'
img.style.transform = 'scale(0.96)'
img.style.zIndex = '999'
img.style.transition = 'all 0.15s cubic-bezier(0.25, 0.46, 0.45, 0.94)'
// 添加拖拽指示器
const indicator = document.createElement('div')
indicator.className = 'drag-indicator'
indicator.style.position = 'fixed'
indicator.style.top = '50%'
indicator.style.left = '50%'
indicator.style.transform = 'translate(-50%, -50%)'
indicator.style.padding = '8px 16px'
indicator.style.background = 'rgba(0, 0, 0, 0.85)'
indicator.style.color = 'white'
indicator.style.borderRadius = '16px'
indicator.style.fontSize = '14px'
indicator.style.fontWeight = '500'
indicator.style.zIndex = '1000'
indicator.style.opacity = '0'
indicator.style.transition = 'opacity 0.1s ease-out'
indicator.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.3)'
indicator.textContent = '拖拽排序'
document.body.appendChild(indicator)
// 渐显指示器
setTimeout(() => {
indicator.style.opacity = '1'
}, 1)
// 保存指示器引用以便后续移除
dragState.value.indicator = indicator
// 添加震动反馈(如果设备支持)
if (navigator.vibrate) {
navigator.vibrate(15)
}
// 阻止页面滚动
e.preventDefault()
}, 500) // 500毫秒长按触发拖拽
}
// 处理触摸移动事件
const handleTouchMove = (e) => {
if (!dragState.value.longPressTimer && !dragState.value.isLongPress) return
const img = dragState.value.draggedImage
const currentX = e.touches[0].clientX
const currentY = e.touches[0].clientY
// 防止图片被选中
e.preventDefault()
// 如果还没有触发长按检查是否移动过多超过6px则取消长按
if (!dragState.value.isLongPress) {
const deltaX = Math.abs(currentX - dragState.value.startX)
const deltaY = Math.abs(currentY - dragState.value.startY)
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY)
if (distance > 6) {
// 移动过多,取消长按
if (dragState.value.longPressTimer) {
clearTimeout(dragState.value.longPressTimer)
dragState.value.longPressTimer = null
}
return
}
}
if (!dragState.value.isLongPress || !img) return
e.preventDefault() // 阻止页面滚动
dragState.value.currentY = currentY
// 计算位移
const deltaY = dragState.value.currentY - dragState.value.startY
// 使用requestAnimationFrame确保流畅的动画
requestAnimationFrame(() => {
// 更新图片位置,添加缓动效果
const easeFactor = 0.95 // 调整缓动因子使拖拽更跟手
img.style.transform = `translateY(${deltaY * easeFactor}px) scale(0.96)`
});
// 使用节流优化,避免过于频繁的检查
if (!dragState.value.lastMoveTime) {
dragState.value.lastMoveTime = 0
}
const now = Date.now()
// 限制检查频率为每16ms一次约60fps提高响应速度
if (now - dragState.value.lastMoveTime >= 16) {
dragState.value.lastMoveTime = now
checkAndSwapImages(img, deltaY)
}
}
// 处理触摸结束事件
const handleTouchEnd = (e) => {
// 清除长按定时器
if (dragState.value.longPressTimer) {
clearTimeout(dragState.value.longPressTimer)
dragState.value.longPressTimer = null
}
if (!dragState.value.isLongPress || !dragState.value.draggedImage) {
dragState.value.isLongPress = false
return
}
// 重置拖拽状态
const img = dragState.value.draggedImage
// 添加更流畅的释放动画
img.style.transition = 'all 0.15s cubic-bezier(0.25, 0.46, 0.45, 0.94)'
img.style.transform = 'translateY(0) scale(1)'
img.style.opacity = '1'
// 移除拖拽指示器
if (dragState.value.indicator) {
const indicator = dragState.value.indicator
indicator.style.transition = 'opacity 0.1s ease-out'
indicator.style.opacity = '0'
setTimeout(() => {
if (indicator.parentNode) {
indicator.parentNode.removeChild(indicator)
}
}, 100)
}
// 添加震动反馈(如果设备支持)
if (navigator.vibrate) {
navigator.vibrate(8)
}
// 延迟重置样式以显示动画
setTimeout(() => {
if (img) {
img.classList.remove('dragging')
img.style.zIndex = ''
img.style.transition = ''
}
}, 150)
// 重置状态
dragState.value.isLongPress = false
dragState.value.draggedImage = null
dragState.value.startY = 0
dragState.value.currentY = 0
dragState.value.indicator = null
// 触发内容更新
handleInput()
}
// 处理触摸取消事件
const handleTouchCancel = (e) => {
// 清除长按定时器
if (dragState.value.longPressTimer) {
clearTimeout(dragState.value.longPressTimer)
dragState.value.longPressTimer = null
}
if (!dragState.value.isLongPress || !dragState.value.draggedImage) {
dragState.value.isLongPress = false
return
}
// 重置拖拽状态
const img = dragState.value.draggedImage
// 添加取消动画
img.style.transition = 'all 0.15s cubic-bezier(0.25, 0.46, 0.45, 0.94)'
img.style.transform = 'translateY(0) scale(1)'
img.style.opacity = '1'
// 移除拖拽指示器
if (dragState.value.indicator) {
const indicator = dragState.value.indicator
indicator.style.transition = 'opacity 0.1s ease-out'
indicator.style.opacity = '0'
setTimeout(() => {
if (indicator.parentNode) {
indicator.parentNode.removeChild(indicator)
}
}, 100)
}
// 延迟重置样式以显示动画
setTimeout(() => {
if (img) {
img.classList.remove('dragging')
img.style.zIndex = ''
img.style.transition = ''
}
}, 150)
// 重置状态
dragState.value.isLongPress = false
dragState.value.draggedImage = null
dragState.value.startY = 0
dragState.value.currentY = 0
dragState.value.indicator = null
}
// 检查并交换图片位置
const checkAndSwapImages = (draggedImg, deltaY) => {
const allImages = Array.from(editorRef.value.querySelectorAll('.editor-image'))
const draggedIndex = allImages.indexOf(draggedImg)
if (draggedIndex === -1) return
// 计算拖拽图片的中心位置
const draggedRect = draggedImg.getBoundingClientRect()
const draggedCenterY = draggedRect.top + draggedRect.height / 2 + deltaY * 0.95 // 调整缓动因子以匹配触摸移动
// 查找最近的图片进行交换
for (let i = 0; i < allImages.length; i++) {
if (i === draggedIndex) continue
const targetImg = allImages[i]
const targetRect = targetImg.getBoundingClientRect()
const targetCenterY = targetRect.top + targetRect.height / 2
// 检查是否与目标图片重叠,使用更精确的碰撞检测
// 当拖拽图片覆盖目标图片高度的60%时触发排序
const overlapThreshold = targetRect.height * 0.6
const distance = Math.abs(draggedCenterY - targetCenterY)
if (distance < overlapThreshold) {
// 直接交换位置,移除视觉反馈以避免闪烁
swapImages(draggedImg, targetImg)
break
}
}
}
// 交换两张图片的位置
const swapImages = (img1, img2) => {
// 为交换添加平滑过渡效果
const parent1 = img1.parentNode
const parent2 = img2.parentNode
// 添加交换动画类
img1.classList.add('swap-animation')
img2.classList.add('swap-animation')
// 如果两张图片在同一父元素中
if (parent1 === parent2) {
// 计算两个图片的位置差
const rect1 = img1.getBoundingClientRect()
const rect2 = img2.getBoundingClientRect()
const deltaY = rect2.top - rect1.top
// 添加临时的变换动画
img1.style.transform = `translateY(${deltaY}px)`
img2.style.transform = `translateY(${-deltaY}px)`
// 在动画完成后交换DOM位置
setTimeout(() => {
// 移除临时变换
img1.style.transform = ''
img2.style.transform = ''
// 移除交换动画类
img1.classList.remove('swap-animation')
img2.classList.remove('swap-animation')
// 交换DOM位置
const tempMarker = document.createElement('div')
parent1.insertBefore(tempMarker, img1)
parent1.insertBefore(img1, img2)
parent1.insertBefore(img2, tempMarker)
tempMarker.remove()
// 触发内容更新
handleInput()
// 自动退出排序模式,提高响应速度
setTimeout(() => {
resetDragState()
}, 10)
}, 200)
} else {
// 不同父元素的情况(更复杂,需要特殊处理)
// 这里简化处理,实际项目中可能需要更复杂的逻辑
// 移除交换动画类
img1.classList.remove('swap-animation')
img2.classList.remove('swap-animation')
const temp = document.createElement('div')
parent1.insertBefore(temp, img1)
parent2.insertBefore(img1, img2)
parent1.insertBefore(img2, temp)
temp.remove()
// 触发内容更新
handleInput()
// 自动退出排序模式,提高响应速度
setTimeout(() => {
resetDragState()
}, 10)
}
}
// 更新工具栏状态
const updateToolbarState = () => {
nextTick(() => {
tools.value.forEach(tool => {
if (tool.name === 'bold') {
tool.active = document.queryCommandState('bold')
} else if (tool.name === 'center') {
tool.active = document.queryCommandState('justifyCenter')
} else if (tool.name === 'list') {
tool.active = document.queryCommandState('insertUnorderedList')
} else if (tool.name === 'quote') {
tool.active = document.queryCommandState('formatBlock')
} else if (tool.name === 'header') {
tool.active = document.queryCommandState('formatBlock')
}
})
})
}
// 处理视口大小变化(用于检测虚拟键盘)
const handleViewportResize = () => {
if (!window.visualViewport) return
const currentHeight = window.visualViewport.height
const heightDifference = initialViewportHeight.value - currentHeight
// 如果高度差超过150px认为虚拟键盘已弹出
isKeyboardVisible.value = heightDifference > 150
// 动态设置CSS变量以调整工具栏位置
document.documentElement.style.setProperty('--viewport-height', `${currentHeight}px`)
// 根据虚拟键盘状态更新工具栏可见性
updateToolbarVisibility()
}
// 处理窗口大小变化(备用方案)
const handleWindowResize = () => {
const currentHeight = window.innerHeight
const heightDifference = initialViewportHeight.value - currentHeight
// 如果高度差超过150px认为虚拟键盘已弹出
isKeyboardVisible.value = heightDifference > 150
// 根据虚拟键盘状态更新工具栏可见性
updateToolbarVisibility()
}
// 更新工具栏可见性
const updateToolbarVisibility = () => {
// 只有当编辑器有焦点且虚拟键盘可见时才显示工具栏
if (isKeyboardVisible.value && (document.activeElement === editorRef.value || isTodoContentActive())) {
isToolbarVisible.value = true
} else {
// 延迟隐藏工具栏,确保用户有时间操作
setTimeout(() => {
// 只有在没有待办事项获得焦点且虚拟键盘不可见时才隐藏工具栏
if (!isTodoContentActive() && !isKeyboardVisible.value) {
isToolbarVisible.value = false
}
}, 200)
}
}
// 检查是否有待办事项内容区域获得焦点
const isTodoContentActive = () => {
const todoContentElements = document.querySelectorAll('.todo-content')
for (let i = 0; i < todoContentElements.length; i++) {
if (todoContentElements[i] === document.activeElement || todoContentElements[i].contains(document.activeElement)) {
return true
}
}
return false
}
// 监听虚拟键盘状态变化
watch(isKeyboardVisible, (newVal) => {
updateToolbarVisibility()
})
// 初始化编辑器内容
onMounted(() => {
if (editorRef.value) {
editorRef.value.innerHTML = content.value
}
})
// 显示工具栏
const showToolbar = () => {
// 只有当虚拟键盘可见时才显示工具栏
if (isKeyboardVisible.value) {
isToolbarVisible.value = true
}
}
// 处理编辑器失焦
const handleBlur = () => {
// 不立即隐藏工具栏而是通过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
}
// 确保工具栏在虚拟键盘可见时保持显示
if (isKeyboardVisible.value) {
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 && !isKeyboardVisible.value) {
isToolbarVisible.value = false
}
}
}, 200) // 增加延迟时间,确保有足够时间处理点击事件
}
// 包装孤立的图片(没有被.image-container包装的图片
const wrapOrphanedImages = () => {
if (!editorRef.value) return;
// 查找所有没有被.image-container包装的图片
const images = editorRef.value.querySelectorAll('img:not(.editor-image)');
console.log('Found orphaned images:', images.length);
images.forEach(img => {
// 检查图片是否已经在.image-container中
if (img.closest('.image-container')) return;
console.log('Wrapping orphaned image');
// 检查图片的父元素是否是.image-container避免嵌套
if (img.parentNode && img.parentNode.classList && img.parentNode.classList.contains('image-container')) {
console.log('Image is already in image-container, checking for delete button');
// 确保图片有正确的类名
img.className = 'editor-image';
img.setAttribute('data-draggable', 'true');
// 为已存在的图片容器添加删除按钮事件监听器
const imgContainer = img.parentNode;
const deleteBtn = imgContainer.querySelector('.image-delete-btn');
if (deleteBtn) {
console.log('Found existing delete button, adding event listeners');
// 先移除可能已有的事件监听器,避免重复
deleteBtn.removeEventListener('click', null);
deleteBtn.removeEventListener('touchend', null);
deleteBtn.addEventListener('click', function(e) {
e.stopPropagation();
imgContainer.remove();
handleInput();
});
deleteBtn.addEventListener('touchend', function(e) {
e.stopPropagation();
imgContainer.remove();
handleInput();
});
// 为图片容器添加短按事件以显示/隐藏删除按钮
// 先移除可能已有的事件监听器,避免重复
const touchStartHandler = imgContainer._touchStartHandler;
const touchEndHandler = imgContainer._touchEndHandler;
if (touchStartHandler) {
imgContainer.removeEventListener('touchstart', touchStartHandler);
}
if (touchEndHandler) {
imgContainer.removeEventListener('touchend', touchEndHandler);
}
let touchStartTime = 0;
const newTouchStartHandler = function(e) {
touchStartTime = Date.now();
};
const newTouchEndHandler = function(e) {
const touchDuration = Date.now() - touchStartTime;
console.log('Touch end event triggered for existing image, duration:', touchDuration, 'isLongPress:', dragState.value.isLongPress);
// 短按小于200ms且非长按拖拽状态时切换删除按钮显示
if (touchDuration < 200 && !dragState.value.isLongPress) {
e.stopPropagation();
console.log('Short tap detected for existing image, toggling delete button visibility');
// 切换删除按钮的显示状态
if (deleteBtn) {
console.log('Current delete button display style for existing image:', deleteBtn.style.display);
// 检查删除按钮当前是否可见
const computedStyle = getComputedStyle(deleteBtn);
const isCurrentlyVisible = deleteBtn.style.display === 'block' ||
computedStyle.display === 'block' ||
(deleteBtn.style.display !== 'none' &&
computedStyle.display !== 'none');
console.log('Delete button current styles for existing image - inline:', deleteBtn.style.display, 'computed:', computedStyle.display);
if (isCurrentlyVisible) {
deleteBtn.style.display = 'none';
console.log('Delete button hidden for existing image');
} else {
deleteBtn.style.display = 'block';
console.log('Delete button displayed for existing image');
}
} else {
console.log('Delete button not found for existing image');
}
} else {
console.log('Not a short tap or isLongPress is true for existing image');
}
};
imgContainer.addEventListener('touchstart', newTouchStartHandler);
imgContainer.addEventListener('touchend', newTouchEndHandler);
// 保存事件处理函数的引用,以便后续移除
imgContainer._touchStartHandler = newTouchStartHandler;
imgContainer._touchEndHandler = newTouchEndHandler;
}
return;
}
// 创建图片容器
const imgContainer = document.createElement('div');
imgContainer.className = 'image-container';
imgContainer.style.position = 'relative';
imgContainer.style.display = 'inline-block';
// 设置图片样式
img.className = 'editor-image';
img.setAttribute('data-draggable', 'true');
img.style.maxWidth = '100%';
img.style.height = 'auto';
img.style.display = 'block';
img.style.objectFit = 'cover';
img.style.boxSizing = 'border-box';
img.style.border = '0.625rem solid white';
img.style.borderRadius = '0.2rem';
img.style.boxShadow = '0 1px 5px rgba(0, 0, 0, 0.18)';
img.style.background = 'var(--background-secondary)';
img.style.position = 'relative';
img.style.outline = 'none';
img.style.userSelect = 'none'; // 防止选中
img.style.webkitUserSelect = 'none'; // 防止选中
img.style.mozUserSelect = 'none'; // 防止选中
img.style.msUserSelect = 'none'; // 防止选中
img.style.webkitTouchCallout = 'none'; // 防止长按弹出菜单
img.style.webkitTapHighlightColor = 'transparent'; // 防止点击高亮
img.draggable = true;
// 创建删除按钮
const deleteBtn = document.createElement('div');
deleteBtn.className = 'image-delete-btn';
deleteBtn.style.cssText = 'position: absolute; top: 8px; right: 8px; width: 24px; height: 24px; cursor: pointer; z-index: 1000; display: none; transition: opacity 0.2s ease; touch-action: manipulation;';
// 将图片和删除按钮添加到容器中
imgContainer.appendChild(img);
imgContainer.appendChild(deleteBtn);
// 替换原来的图片
img.parentNode.replaceChild(imgContainer, img);
// 为新包装的图片添加事件监听器
// 先移除可能已有的事件监听器,避免重复
imgContainer.removeEventListener('touchstart', handleTouchStart)
imgContainer.removeEventListener('touchmove', handleTouchMove)
imgContainer.removeEventListener('touchend', handleTouchEnd)
imgContainer.removeEventListener('touchcancel', handleTouchCancel)
// 重新添加事件监听器
imgContainer.addEventListener('touchstart', handleTouchStart)
imgContainer.addEventListener('touchmove', handleTouchMove)
imgContainer.addEventListener('touchend', handleTouchEnd)
imgContainer.addEventListener('touchcancel', handleTouchCancel)
// 为删除按钮添加点击事件
// 先移除可能已有的事件监听器,避免重复
deleteBtn.removeEventListener('click', null)
deleteBtn.removeEventListener('touchend', null)
deleteBtn.addEventListener('click', function(e) {
e.stopPropagation();
imgContainer.remove();
handleInput();
});
deleteBtn.addEventListener('touchend', function(e) {
e.stopPropagation();
imgContainer.remove();
handleInput();
});
// 为图片容器添加短按事件以显示/隐藏删除按钮
let touchStartTime = 0;
const touchStartHandler = function(e) {
touchStartTime = Date.now();
};
const touchEndHandler = function(e) {
const touchDuration = Date.now() - touchStartTime;
console.log('Touch end event triggered in wrapOrphanedImages, duration:', touchDuration, 'isLongPress:', dragState.value.isLongPress);
// 短按小于200ms且非长按拖拽状态时切换删除按钮显示
if (touchDuration < 200 && !dragState.value.isLongPress) {
e.stopPropagation();
console.log('Short tap detected in wrapOrphanedImages, toggling delete button visibility');
// 切换删除按钮的显示状态
if (deleteBtn) {
console.log('Current delete button display style in wrapOrphanedImages:', deleteBtn.style.display);
// 检查删除按钮当前是否可见
const computedStyle = getComputedStyle(deleteBtn);
const isCurrentlyVisible = deleteBtn.style.display === 'block' ||
computedStyle.display === 'block' ||
(deleteBtn.style.display !== 'none' &&
computedStyle.display !== 'none');
console.log('Delete button current styles - inline:', deleteBtn.style.display, 'computed:', computedStyle.display);
console.log('Delete button background image:', computedStyle.backgroundImage);
console.log('Delete button width:', computedStyle.width, 'height:', computedStyle.height);
console.log('Delete button position:', computedStyle.position);
console.log('Delete button z-index:', computedStyle.zIndex);
if (isCurrentlyVisible) {
deleteBtn.style.display = 'none';
console.log('Delete button hidden in wrapOrphanedImages');
} else {
deleteBtn.style.display = 'block';
console.log('Delete button displayed in wrapOrphanedImages');
}
} else {
console.log('Delete button not found in wrapOrphanedImages');
}
} else {
console.log('Not a short tap or isLongPress is true in wrapOrphanedImages');
}
};
imgContainer.addEventListener('touchstart', touchStartHandler);
imgContainer.addEventListener('touchend', touchEndHandler);
// 保存事件处理函数的引用,以便后续移除
imgContainer._touchStartHandler = touchStartHandler;
imgContainer._touchEndHandler = touchEndHandler;
});
}
// 调整已有图片的高度
const adjustExistingImages = () => {
console.log('Adjusting existing images')
// 等待DOM更新完成
setTimeout(() => {
if (editorRef.value) {
const imageContainers = editorRef.value.querySelectorAll('.image-container')
console.log('Found image containers:', imageContainers.length)
imageContainers.forEach(container => {
const img = container.querySelector('img.editor-image')
if (!img) return
console.log('Processing image:', img)
// 只处理还没有调整过高度的图片
if (!img.dataset.heightAdjusted) {
console.log('Adjusting height for image')
// 创建一个临时图片来获取原始尺寸
const tempImg = new Image()
tempImg.onload = function () {
// 获取CSS变量
const editorFontSize = getComputedStyle(document.documentElement).getPropertyValue('--editor-font-size').trim() || '16px'
const editorLineHeight = getComputedStyle(document.documentElement).getPropertyValue('--editor-line-height').trim() || '1.6'
const fontSize = parseInt(editorFontSize)
const lineHeight = parseFloat(editorLineHeight)
// 计算行高
const computedLineHeight = fontSize * lineHeight
// 获取编辑器的宽度(减去一些内边距)
const editorWidth = editorRef.value.offsetWidth - 20 // 20px为左右内边距
// 按宽度撑满计算调整后的尺寸
const originalHeight = tempImg.height
const originalWidth = tempImg.width
const scaleRatio = editorWidth / originalWidth
const scaledHeight = originalHeight * scaleRatio
// 计算调整后的高度,使其为行高的整数倍
const scaleFactor = Math.max(1, Math.round(scaledHeight / computedLineHeight))
const adjustedHeight = scaleFactor * computedLineHeight
// 按比例调整宽度
const adjustedWidth = (originalWidth * adjustedHeight) / originalHeight
img.style.height = `${adjustedHeight}px`
img.style.width = `${adjustedWidth}px`
// 确保图片与基准线对齐
img.style.verticalAlign = 'top'
// 标记图片已调整过高度
img.dataset.heightAdjusted = 'true'
console.log('Adjusted image dimensions:', adjustedWidth, adjustedHeight)
}
tempImg.src = img.src
}
})
// 为现有图片添加拖拽功能和删除按钮功能
imageContainers.forEach(container => {
const img = container.querySelector('img.editor-image')
if (!img) return
console.log('Adding drag functionality to image:', img)
// 为图片容器添加事件监听器(总是添加,确保功能正常)
// 先移除可能已有的事件监听器,避免重复
container.removeEventListener('touchstart', handleTouchStart)
container.removeEventListener('touchmove', handleTouchMove)
container.removeEventListener('touchend', handleTouchEnd)
container.removeEventListener('touchcancel', handleTouchCancel)
// 重新添加事件监听器
container.addEventListener('touchstart', handleTouchStart)
container.addEventListener('touchmove', handleTouchMove)
container.addEventListener('touchend', handleTouchEnd)
container.addEventListener('touchcancel', handleTouchCancel)
// 为删除按钮添加点击事件
let deleteBtn = container.querySelector('.image-delete-btn')
if (!deleteBtn) {
// 如果删除按钮不存在,创建它
console.log('Delete button not found, creating new one')
deleteBtn = document.createElement('div')
deleteBtn.className = 'image-delete-btn'
deleteBtn.style.cssText = 'position: absolute; top: 8px; right: 8px; width: 24px; height: 24px; cursor: pointer; z-index: 1000; display: none; transition: opacity 0.2s ease; touch-action: manipulation;'
container.appendChild(deleteBtn)
}
if (deleteBtn) {
// 先移除可能已有的事件监听器,避免重复
deleteBtn.removeEventListener('click', null)
deleteBtn.removeEventListener('touchend', null)
deleteBtn.addEventListener('click', function(e) {
e.stopPropagation();
container.remove();
handleInput();
});
deleteBtn.addEventListener('touchend', function(e) {
e.stopPropagation();
container.remove();
handleInput();
});
}
// 为图片容器添加短按事件以显示/隐藏删除按钮
// 先移除可能已有的事件监听器,避免重复
const touchStartHandler = container._touchStartHandler;
const touchEndHandler = container._touchEndHandler;
if (touchStartHandler) {
container.removeEventListener('touchstart', touchStartHandler);
}
if (touchEndHandler) {
container.removeEventListener('touchend', touchEndHandler);
}
let touchStartTime = 0;
const newTouchStartHandler = function(e) {
touchStartTime = Date.now();
};
const newTouchEndHandler = function(e) {
const touchDuration = Date.now() - touchStartTime;
console.log('Touch end event triggered in adjustExistingImages, duration:', touchDuration, 'isLongPress:', dragState.value.isLongPress);
// 短按小于200ms且非长按拖拽状态时切换删除按钮显示
if (touchDuration < 200 && !dragState.value.isLongPress) {
e.stopPropagation();
console.log('Short tap detected in adjustExistingImages, toggling delete button visibility');
// 切换删除按钮的显示状态
if (deleteBtn) {
console.log('Current delete button display style in adjustExistingImages:', deleteBtn.style.display);
// 检查删除按钮当前是否可见
const computedStyle = getComputedStyle(deleteBtn);
const isCurrentlyVisible = deleteBtn.style.display === 'block' ||
computedStyle.display === 'block' ||
(deleteBtn.style.display !== 'none' &&
computedStyle.display !== 'none');
console.log('Delete button current styles - inline:', deleteBtn.style.display, 'computed:', computedStyle.display);
if (isCurrentlyVisible) {
deleteBtn.style.display = 'none';
console.log('Delete button hidden in adjustExistingImages');
} else {
deleteBtn.style.display = 'block';
console.log('Delete button displayed in adjustExistingImages');
}
} else {
console.log('Delete button not found in adjustExistingImages');
}
} else {
console.log('Not a short tap or isLongPress is true in adjustExistingImages');
}
};
container.addEventListener('touchstart', newTouchStartHandler);
container.addEventListener('touchend', newTouchEndHandler);
// 保存事件处理函数的引用,以便后续移除
container._touchStartHandler = newTouchStartHandler;
container._touchEndHandler = newTouchEndHandler;
img.setAttribute('data-touch-listeners', 'true')
console.log('Added touch event listeners')
})
}
}, 0)
}
// 清理动态添加的属性(仅在保存时移除临时属性,保留必要属性)
const cleanContentForSave = () => {
if (!editorRef.value) return content.value;
// 创建一个临时的div来操作内容
const tempDiv = document.createElement('div');
tempDiv.innerHTML = editorRef.value.innerHTML;
// 移除图片上的临时动态属性
const images = tempDiv.querySelectorAll('img.editor-image');
images.forEach(img => {
// 移除拖拽时的临时样式属性
img.style.removeProperty('z-index');
img.style.removeProperty('transition');
img.style.removeProperty('transform');
img.style.removeProperty('opacity');
// 移除拖拽时的临时类名
img.classList.remove('dragging');
img.classList.remove('swap-animation');
// 移除临时的数据属性保留必要的属性如data-draggable
img.removeAttribute('data-height-adjusted');
img.removeAttribute('data-touch-listeners');
});
// 移除拖拽指示器(如果存在)
const indicators = tempDiv.querySelectorAll('.drag-indicator');
indicators.forEach(indicator => {
indicator.remove();
});
return tempDiv.innerHTML;
};
// 暴露方法给父组件
defineExpose({
getContent: () => cleanContentForSave(),
setContent: newContent => {
console.log('Setting content:', newContent)
content.value = newContent || ''
if (editorRef.value) {
try {
editorRef.value.innerHTML = content.value
console.log('Content set successfully in editorRef')
// 重置拖拽状态确保isLongPress为false
dragState.value.isLongPress = false
dragState.value.draggedImage = null
dragState.value.startX = 0
dragState.value.startY = 0
dragState.value.currentY = 0
// 确保所有图片都被正确包装在.image-container中
wrapOrphanedImages()
// 调整已有图片的高度并添加拖拽功能
adjustExistingImages()
// 为图片添加拖拽事件监听器
setTimeout(() => {
console.log('Adding drag event listeners to images in setContent')
const imageContainers = editorRef.value.querySelectorAll('.image-container')
console.log('Found image containers in setContent:', imageContainers.length)
imageContainers.forEach(container => {
const img = container.querySelector('img.editor-image')
if (!img) return
console.log('Adding touch listeners to image in setContent:', img)
// 先移除可能已有的事件监听器,避免重复
container.removeEventListener('touchstart', handleTouchStart)
container.removeEventListener('touchmove', handleTouchMove)
container.removeEventListener('touchend', handleTouchEnd)
container.removeEventListener('touchcancel', handleTouchCancel)
// 重新添加事件监听器
container.addEventListener('touchstart', handleTouchStart)
container.addEventListener('touchmove', handleTouchMove)
container.addEventListener('touchend', handleTouchEnd)
container.addEventListener('touchcancel', handleTouchCancel)
// 为删除按钮添加点击事件
const deleteBtn = container.querySelector('.image-delete-btn')
if (deleteBtn) {
// 先移除可能已有的事件监听器,避免重复
deleteBtn.removeEventListener('click', null)
deleteBtn.removeEventListener('touchend', null)
deleteBtn.addEventListener('click', function(e) {
e.stopPropagation();
container.remove();
handleInput();
});
deleteBtn.addEventListener('touchend', function(e) {
e.stopPropagation();
container.remove();
handleInput();
});
}
// 为图片容器添加短按事件以显示/隐藏删除按钮
// 先移除可能已有的事件监听器,避免重复
const touchStartHandler = container._touchStartHandler;
const touchEndHandler = container._touchEndHandler;
if (touchStartHandler) {
container.removeEventListener('touchstart', touchStartHandler);
}
if (touchEndHandler) {
container.removeEventListener('touchend', touchEndHandler);
}
let touchStartTime = 0;
const newTouchStartHandler = function(e) {
touchStartTime = Date.now();
};
const newTouchEndHandler = function(e) {
const touchDuration = Date.now() - touchStartTime;
console.log('Touch end event triggered in setContent, duration:', touchDuration, 'isLongPress:', dragState.value.isLongPress);
// 短按小于200ms且非长按拖拽状态时切换删除按钮显示
if (touchDuration < 200 && !dragState.value.isLongPress) {
e.stopPropagation();
console.log('Short tap detected in setContent, toggling delete button visibility');
// 切换删除按钮的显示状态
if (deleteBtn) {
console.log('Current delete button display style in setContent:', deleteBtn.style.display);
// 检查删除按钮当前是否可见
const computedStyle = getComputedStyle(deleteBtn);
const isCurrentlyVisible = deleteBtn.style.display === 'block' ||
computedStyle.display === 'block' ||
(deleteBtn.style.display !== 'none' &&
computedStyle.display !== 'none');
console.log('Delete button current styles - inline:', deleteBtn.style.display, 'computed:', computedStyle.display);
console.log('Delete button background image:', computedStyle.backgroundImage);
console.log('Delete button width:', computedStyle.width, 'height:', computedStyle.height);
console.log('Delete button position:', computedStyle.position);
console.log('Delete button z-index:', computedStyle.zIndex);
if (isCurrentlyVisible) {
deleteBtn.style.display = 'none';
console.log('Delete button hidden in setContent');
} else {
deleteBtn.style.display = 'block';
console.log('Delete button displayed in setContent');
// 添加调试样式以确保可见
deleteBtn.style.backgroundColor = 'rgba(255, 0, 0, 0.3)'; // 半透明红色背景用于调试
}
} else {
console.log('Delete button not found in setContent');
}
} else {
console.log('Not a short tap or isLongPress is true in setContent');
}
};
container.addEventListener('touchstart', newTouchStartHandler);
container.addEventListener('touchend', newTouchEndHandler);
// 保存事件处理函数的引用,以便后续移除
container._touchStartHandler = newTouchStartHandler;
container._touchEndHandler = newTouchEndHandler;
img.setAttribute('data-touch-listeners', 'true')
console.log('Added touch event listeners')
})
}, 0)
} catch (error) {
console.error('Failed to set innerHTML:', error)
// 备选方案使用textContent
try {
editorRef.value.textContent = content.value
console.log('Content set using textContent')
} catch (textContentError) {
console.error('Failed to set textContent:', textContentError)
}
}
} else {
// 如果editorRef还不可用延迟设置
console.log('Editor ref is not available, will retry when mounted')
setTimeout(() => {
if (editorRef.value) {
try {
editorRef.value.innerHTML = content.value
console.log('Content set successfully after delay')
// 重置拖拽状态确保isLongPress为false
dragState.value.isLongPress = false
dragState.value.draggedImage = null
dragState.value.startX = 0
dragState.value.startY = 0
dragState.value.currentY = 0
// 确保所有图片都被正确包装在.image-container中
wrapOrphanedImages()
// 调整已有图片的高度并添加拖拽功能
adjustExistingImages()
// 为图片添加拖拽事件监听器
setTimeout(() => {
console.log('Adding drag event listeners to images in delayed setContent')
const imageContainers = editorRef.value.querySelectorAll('.image-container')
console.log('Found image containers in delayed setContent:', imageContainers.length)
imageContainers.forEach(container => {
const img = container.querySelector('img.editor-image')
if (!img) return
console.log('Adding touch listeners to image in delayed setContent:', img)
// 先移除可能已有的事件监听器,避免重复
container.removeEventListener('touchstart', handleTouchStart)
container.removeEventListener('touchmove', handleTouchMove)
container.removeEventListener('touchend', handleTouchEnd)
container.removeEventListener('touchcancel', handleTouchCancel)
// 重新添加事件监听器
container.addEventListener('touchstart', handleTouchStart)
container.addEventListener('touchmove', handleTouchMove)
container.addEventListener('touchend', handleTouchEnd)
container.addEventListener('touchcancel', handleTouchCancel)
// 为删除按钮添加点击事件
const deleteBtn = container.querySelector('.image-delete-btn')
if (deleteBtn) {
// 先移除可能已有的事件监听器,避免重复
deleteBtn.removeEventListener('click', null)
deleteBtn.removeEventListener('touchend', null)
deleteBtn.addEventListener('click', function(e) {
e.stopPropagation();
container.remove();
handleInput();
});
deleteBtn.addEventListener('touchend', function(e) {
e.stopPropagation();
container.remove();
handleInput();
});
}
// 为图片容器添加短按事件以显示/隐藏删除按钮
// 先移除可能已有的事件监听器,避免重复
const touchStartHandler = container._touchStartHandler;
const touchEndHandler = container._touchEndHandler;
if (touchStartHandler) {
container.removeEventListener('touchstart', touchStartHandler);
}
if (touchEndHandler) {
container.removeEventListener('touchend', touchEndHandler);
}
let touchStartTime = 0;
const newTouchStartHandler = function(e) {
touchStartTime = Date.now();
};
const newTouchEndHandler = function(e) {
const touchDuration = Date.now() - touchStartTime;
console.log('Touch end event triggered in delayed setContent, duration:', touchDuration, 'isLongPress:', dragState.value.isLongPress);
// 短按小于200ms且非长按拖拽状态时切换删除按钮显示
if (touchDuration < 200 && !dragState.value.isLongPress) {
e.stopPropagation();
console.log('Short tap detected in delayed setContent, toggling delete button visibility');
// 切换删除按钮的显示状态
if (deleteBtn) {
console.log('Current delete button display style in delayed setContent:', deleteBtn.style.display);
// 检查删除按钮当前是否可见
const computedStyle = getComputedStyle(deleteBtn);
const isCurrentlyVisible = deleteBtn.style.display === 'block' ||
computedStyle.display === 'block' ||
(deleteBtn.style.display !== 'none' &&
computedStyle.display !== 'none');
console.log('Delete button current styles - inline:', deleteBtn.style.display, 'computed:', computedStyle.display);
if (isCurrentlyVisible) {
deleteBtn.style.display = 'none';
console.log('Delete button hidden in delayed setContent');
} else {
deleteBtn.style.display = 'block';
console.log('Delete button displayed in delayed setContent');
}
} else {
console.log('Delete button not found in delayed setContent');
}
} else {
console.log('Not a short tap or isLongPress is true in delayed setContent');
}
};
container.addEventListener('touchstart', newTouchStartHandler);
container.addEventListener('touchend', newTouchEndHandler);
// 保存事件处理函数的引用,以便后续移除
container._touchStartHandler = newTouchStartHandler;
container._touchEndHandler = newTouchEndHandler;
img.setAttribute('data-touch-listeners', 'true')
console.log('Added touch event listeners')
})
}, 0)
} catch (error) {
console.error('Failed to set innerHTML after delay:', error)
}
}
}, 100)
}
},
insertImage,
})
</script>
<style scoped>
.editor-container {
display: flex;
flex-direction: column;
min-height: 100%;
background-color: var(--background-card);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
}
.toolbar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
display: flex;
padding: 0.5rem 0.75rem;
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.keyboard-visible {
bottom: 0;
}
/* 使用visualViewport API动态调整工具栏位置 */
.toolbar.dynamic-position {
bottom: calc(100vh - var(--viewport-height, 100vh));
}
.toolbar.visible {
transform: translateY(0);
}
.toolbar-btn {
padding: 0.375rem;
margin-right: 0.375rem;
border: none;
background: transparent;
cursor: pointer;
border-radius: 0.25rem;
transition: background-color 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
/* 确保按钮能正确获取焦点 */
tabindex: 0;
}
.toolbar-btn:hover {
background-color: var(--background-secondary);
}
.toolbar-btn.active {
background-color: var(--primary-light);
border: 1px solid var(--border);
}
.toolbar-icon {
width: 1.25rem;
height: 1.25rem;
opacity: 0.8;
}
.toolbar-btn.active .toolbar-icon {
opacity: 1;
}
.editor-content {
flex: 1;
padding: 0 0.625rem 3.75rem 0.625rem; /* 添加底部内边距,防止内容被工具栏遮挡 */
outline: none;
overflow-y: auto;
font-size: var(--editor-font-size, 1rem);
line-height: var(--editor-line-height, 1.6);
color: var(--note-content);
min-height: 12.5rem;
background-color: var(--background-card);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
position: relative;
/* 基准线样式 */
background-image: linear-gradient(to bottom, var(--border) 1px, transparent 1px);
background-size: 100% calc(var(--editor-font-size, 1rem) * var(--editor-line-height, 1.6)); /* var(--editor-font-size) * var(--editor-line-height) */
background-repeat: repeat-y;
background-position: 0 calc((var(--editor-font-size, 1rem) * var(--editor-line-height, 1.6) - var(--editor-font-size, 1rem)) / 2);
}
.editor-content::before {
display: none;
}
/* 统一处理所有直接子元素的间距 */
.editor-content > * {
margin: 0;
padding: 0;
}
.editor-content:focus,
.editor-content * {
outline: none;
}
/* 优化段落样式,确保与基准线对齐 */
:deep(.editor-content p) {
margin: 0 0 0.75rem 0;
line-height: var(--editor-line-height, 1.6);
letter-spacing: 0.3px;
}
/* 自定义内容样式 - 统一行高和间距 */
:deep(.editor-content h2) {
font-size: var(--editor-font-size, 1rem);
font-weight: 600;
margin: 0 0 0.75rem 0;
color: var(--note-title);
line-height: var(--editor-line-height, 1.6);
letter-spacing: 0.3px;
text-align: center;
position: relative;
}
:deep(.editor-content blockquote) {
border-left: 3px solid var(--primary);
padding: 0 1rem 0 1rem;
margin: 0 0 0.75rem 0;
color: var(--text-secondary);
background-color: var(--background-secondary);
font-style: italic;
line-height: var(--editor-line-height, 1.6);
}
:deep(.quote-container) {
position: relative;
margin: 0 0 0.75rem 0;
line-height: var(--editor-line-height, 1.6);
}
:deep(.quote-icon) {
position: absolute;
left: 0;
top: 0;
width: var(--editor-font-size, 1rem);
height: var(--editor-font-size, 1rem);
margin-top: 0.1875rem;
}
:deep(.quote-content) {
border-left: 3px solid var(--primary);
padding: 0 var(--editor-font-size, 1rem) 0 2rem;
margin-left: var(--editor-font-size, 1rem);
color: var(--text-secondary);
background-color: var(--background-secondary);
font-style: italic;
line-height: var(--editor-line-height, 1.6);
}
:deep(.editor-content ul),
:deep(.editor-content ol) {
margin: 0 0 0.75rem 0;
padding-left: 2rem;
position: relative;
line-height: var(--editor-line-height, 1.6);
}
:deep(.editor-content li) {
margin: 0;
line-height: var(--editor-line-height, 1.6);
padding: 0;
}
:deep(.editor-content strong) {
color: var(--text-primary);
font-weight: 600;
}
:deep(.editor-content em) {
color: var(--text-secondary);
font-style: italic;
}
:deep(.editor-content u) {
text-decoration: none;
border-bottom: 1px solid var(--text-primary);
}
:deep(.editor-content hr) {
border: none;
height: 1px;
background-color: var(--border);
margin: 0.75rem 0;
}
.editor-content div[style*='text-align: center'] {
text-align: center;
}
:deep(.editor-content .image-container) {
display: inline-block;
position: relative;
margin: calc((var(--editor-line-height, 1.6) * 10) * 1px) auto;
}
:deep(.editor-content .editor-image) {
max-width: 100%;
height: auto;
display: block;
object-fit: cover;
box-sizing: border-box;
border: 0.625rem solid white;
border-radius: 0.2rem;
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.18);
background: var(--background-secondary);
position: relative;
outline: none; /* 移除默认焦点轮廓 */
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
-webkit-touch-callout: none;
-webkit-tap-highlight-color: transparent;
}
:deep(.editor-content .image-delete-btn) {
position: absolute;
top: 8px;
right: 8px;
width: 24px;
height: 24px;
cursor: pointer;
z-index: 1000;
display: none;
transition: opacity 0.2s ease;
/* 使用背景图片而不是背景色和边框,确保图标正确显示 */
background-image: url('/assets/icons/drawable-xxhdpi/item_image_btn_unbrella_delete.png');
background-size: contain;
background-repeat: no-repeat;
background-position: center;
background-color: transparent; /* 确保背景透明 */
}
:deep(.editor-content .editor-image.draggable) {
cursor: move;
}
:deep(.editor-content .editor-image.dragging) {
opacity: 0.85;
transform: scale(0.96);
z-index: 999;
transition: transform 0.15s cubic-bezier(0.25, 0.46, 0.45, 0.94), opacity 0.15s ease; /* 优化过渡效果 */
box-shadow: 0 12px 25px rgba(0, 0, 0, 0.22);
will-change: transform, opacity; /* 提示浏览器优化这些属性 */
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
/* 图片交换动画 */
:deep(.editor-content .editor-image.swap-animation) {
transition: transform 0.2s ease-out;
}
/* 待办事项样式 */
:deep(.todo-container) {
display: flex;
align-items: flex-start;
margin: 0;
line-height: var(--editor-line-height, 1.6);
position: relative;
padding-left: calc(var(--editor-font-size, 1rem) * 1.5);
}
:deep(.todo-icon) {
width: calc(var(--editor-font-size, 1rem) * 1.5);
height: calc(var(--editor-font-size, 1rem) * 1.5);
cursor: pointer;
flex-shrink: 0;
position: absolute;
top: 50%;
left: 0;
transform: translateY(-50%);
}
:deep(.todo-content) {
flex: 1;
outline: none;
line-height: var(--editor-line-height, 1.6);
padding: 0;
margin: 0;
}
</style>