You've already forked SmartisanNote.Remake
2624 lines
96 KiB
Vue
2624 lines
96 KiB
Vue
<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>
|