✨ 实现图片编辑功能,优化图片交互体验
核心功能: - 双击图片 → 进入编辑模式(替换图片) - 右键图片 → 显示菜单(预览/编辑选项) - 图片编辑器支持文件选择和替换 - 支持HTML图片和MindElixir原生图片两种类型 技术实现: - 修改mouse.ts,双击图片触发showImageEditor事件 - 扩展contextMenu.ts,添加图片编辑菜单项 - 更新pubsub.ts,添加showImageEditor事件类型 - 在MindMap.vue中实现图片编辑器UI和逻辑 - 添加文件选择、格式验证、大小限制(5MB) - 支持JPG/PNG/GIF格式 UI设计: - 紫色主题编辑器(与品牌色一致) - 当前图片预览 + 文件选择界面 - 响应式设计,支持大图片显示 - 优雅的模态框和交互体验
This commit is contained in:
parent
1719ad4c8a
commit
e3fedcbf0f
|
|
@ -164,6 +164,40 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 图片编辑器模态框 -->
|
||||||
|
<div v-if="showImageEditor" class="image-editor-modal" @click="closeImageEditor">
|
||||||
|
<div class="image-editor-content" @click.stop>
|
||||||
|
<div class="image-editor-header">
|
||||||
|
<span class="image-editor-title">{{ imageEditorTitle }}</span>
|
||||||
|
<button @click="closeImageEditor" class="image-editor-close">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="image-editor-body">
|
||||||
|
<div class="current-image-section">
|
||||||
|
<h4>当前图片:</h4>
|
||||||
|
<img :src="imageEditorUrl" :alt="imageEditorTitle" class="current-image" />
|
||||||
|
</div>
|
||||||
|
<div class="replace-image-section">
|
||||||
|
<h4>替换图片:</h4>
|
||||||
|
<div class="file-input-wrapper">
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
@change="handleImageFileSelect"
|
||||||
|
accept="image/*"
|
||||||
|
id="imageFileInput"
|
||||||
|
class="file-input"
|
||||||
|
/>
|
||||||
|
<label for="imageFileInput" class="file-input-label">
|
||||||
|
📁 选择新图片
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<p class="file-hint">支持 JPG、PNG、GIF 格式,最大 5MB</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="image-editor-footer">
|
||||||
|
<button @click="closeImageEditor" class="cancel-button">取消</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -256,6 +290,12 @@ const imagePreviewTitle = ref('');
|
||||||
const imagePreviewLoading = ref(false);
|
const imagePreviewLoading = ref(false);
|
||||||
const imagePreviewError = ref('');
|
const imagePreviewError = ref('');
|
||||||
|
|
||||||
|
// 图片编辑相关状态
|
||||||
|
const showImageEditor = ref(false);
|
||||||
|
const imageEditorUrl = ref('');
|
||||||
|
const imageEditorTitle = ref('');
|
||||||
|
const currentImageElement = ref(null);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -340,6 +380,69 @@ const closeImagePreview = () => {
|
||||||
imagePreviewError.value = '';
|
imagePreviewError.value = '';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 图片编辑器相关函数
|
||||||
|
const openImageEditor = (imageUrl, altText, imgElement) => {
|
||||||
|
console.log('🖼️ 打开图片编辑器:', { imageUrl, altText });
|
||||||
|
|
||||||
|
imageEditorUrl.value = imageUrl;
|
||||||
|
imageEditorTitle.value = altText || '编辑图片';
|
||||||
|
currentImageElement.value = imgElement;
|
||||||
|
showImageEditor.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeImageEditor = () => {
|
||||||
|
showImageEditor.value = false;
|
||||||
|
imageEditorUrl.value = '';
|
||||||
|
imageEditorTitle.value = '';
|
||||||
|
currentImageElement.value = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleImageFileSelect = (event) => {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
// 检查文件类型
|
||||||
|
if (!file.type.startsWith('image/')) {
|
||||||
|
alert('请选择图片文件');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查文件大小(限制为5MB)
|
||||||
|
if (file.size > 5 * 1024 * 1024) {
|
||||||
|
alert('图片文件不能超过5MB');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
const newImageUrl = e.target.result;
|
||||||
|
updateImageInNode(newImageUrl);
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateImageInNode = (newImageUrl) => {
|
||||||
|
if (currentImageElement.value) {
|
||||||
|
// 更新HTML中的图片元素
|
||||||
|
currentImageElement.value.src = newImageUrl;
|
||||||
|
console.log('🖼️ 已更新HTML图片元素');
|
||||||
|
} else {
|
||||||
|
// 更新MindElixir原生图片属性
|
||||||
|
const currentNode = mindElixir.value?.currentNode;
|
||||||
|
if (currentNode && currentNode.nodeObj) {
|
||||||
|
if (typeof currentNode.nodeObj.image === 'string') {
|
||||||
|
currentNode.nodeObj.image = newImageUrl;
|
||||||
|
} else if (currentNode.nodeObj.image && typeof currentNode.nodeObj.image === 'object') {
|
||||||
|
currentNode.nodeObj.image.url = newImageUrl;
|
||||||
|
}
|
||||||
|
console.log('🖼️ 已更新MindElixir原生图片属性');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭编辑器
|
||||||
|
closeImageEditor();
|
||||||
|
};
|
||||||
|
|
||||||
const onImageLoad = () => {
|
const onImageLoad = () => {
|
||||||
console.log('✅ 模态框图片加载成功');
|
console.log('✅ 模态框图片加载成功');
|
||||||
// 图片已经在预加载阶段处理了状态,这里只是确认
|
// 图片已经在预加载阶段处理了状态,这里只是确认
|
||||||
|
|
@ -636,6 +739,12 @@ const loadMindmapData = async (data, keepPosition = false, shouldCenterRoot = tr
|
||||||
openImagePreview(imageUrl, altText);
|
openImagePreview(imageUrl, altText);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 添加图片编辑事件监听器
|
||||||
|
mindElixir.value.bus.addListener('showImageEditor', (imageUrl, altText, imgElement) => {
|
||||||
|
console.log('🖼️ 收到图片编辑事件:', { imageUrl, altText });
|
||||||
|
openImageEditor(imageUrl, altText, imgElement);
|
||||||
|
});
|
||||||
|
|
||||||
// Mind Elixir现在会自动使用markdown解析器渲染内容
|
// Mind Elixir现在会自动使用markdown解析器渲染内容
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -724,6 +833,12 @@ const loadMindmapData = async (data, keepPosition = false, shouldCenterRoot = tr
|
||||||
openImagePreview(imageUrl, altText);
|
openImagePreview(imageUrl, altText);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 添加图片编辑事件监听器
|
||||||
|
mindElixir.value.bus.addListener('showImageEditor', (imageUrl, altText, imgElement) => {
|
||||||
|
console.log('🖼️ 收到图片编辑事件(延迟创建):', { imageUrl, altText });
|
||||||
|
openImageEditor(imageUrl, altText, imgElement);
|
||||||
|
});
|
||||||
|
|
||||||
// 延迟执行后续操作
|
// 延迟执行后续操作
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// Mind Elixir现在会自动使用markdown解析器渲染内容
|
// Mind Elixir现在会自动使用markdown解析器渲染内容
|
||||||
|
|
@ -4881,5 +4996,153 @@ const updateMindMapRealtime = async (data, title) => {
|
||||||
background: #7d0a8e;
|
background: #7d0a8e;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 图片编辑器模态框样式 */
|
||||||
|
.image-editor-modal {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.8);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 10001;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-editor-content {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
width: 600px;
|
||||||
|
max-width: 90vw;
|
||||||
|
max-height: 80vh;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
|
||||||
|
border: 2px solid #660874;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-editor-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-editor-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-editor-close {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 28px;
|
||||||
|
color: #666;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-editor-close:hover {
|
||||||
|
background: #e0e0e0;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-editor-body {
|
||||||
|
padding: 20px;
|
||||||
|
max-height: 60vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-image-section {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-image-section h4 {
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
color: #333;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-image {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 200px;
|
||||||
|
height: auto;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.replace-image-section h4 {
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
color: #333;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-input-wrapper {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-input-label {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 10px 20px;
|
||||||
|
background: #660874;
|
||||||
|
color: white;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: background 0.2s;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-input-label:hover {
|
||||||
|
background: #4d0655;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-hint {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-editor-footer {
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-top: 1px solid #e0e0e0;
|
||||||
|
background: #f9f9f9;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancel-button {
|
||||||
|
padding: 8px 20px;
|
||||||
|
background: #6c757d;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancel-button:hover {
|
||||||
|
background: #5a6268;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,212 +1,212 @@
|
||||||
import { handleZoom } from './plugin/keypress'
|
import { handleZoom } from './plugin/keypress'
|
||||||
import type { SummarySvgGroup } from './summary'
|
import type { SummarySvgGroup } from './summary'
|
||||||
import type { Expander, CustomSvg, Topic } from './types/dom'
|
import type { Expander, CustomSvg, Topic } from './types/dom'
|
||||||
import type { MindElixirInstance } from './types/index'
|
import type { MindElixirInstance } from './types/index'
|
||||||
import { isTopic, on } from './utils'
|
import { isTopic, on } from './utils'
|
||||||
|
|
||||||
export default function (mind: MindElixirInstance) {
|
export default function (mind: MindElixirInstance) {
|
||||||
const { dragMoveHelper } = mind
|
const { dragMoveHelper } = mind
|
||||||
|
|
||||||
const handleClick = (e: MouseEvent) => {
|
const handleClick = (e: MouseEvent) => {
|
||||||
console.log('handleClick', e)
|
console.log('handleClick', e)
|
||||||
// Only handle primary button clicks
|
// Only handle primary button clicks
|
||||||
if (e.button !== 0) return
|
if (e.button !== 0) return
|
||||||
if (mind.helper1?.moved) {
|
if (mind.helper1?.moved) {
|
||||||
mind.helper1.clear()
|
mind.helper1.clear()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (mind.helper2?.moved) {
|
if (mind.helper2?.moved) {
|
||||||
mind.helper2.clear()
|
mind.helper2.clear()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (dragMoveHelper.moved) {
|
if (dragMoveHelper.moved) {
|
||||||
dragMoveHelper.clear()
|
dragMoveHelper.clear()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const target = e.target as HTMLElement
|
const target = e.target as HTMLElement
|
||||||
if (target.tagName === 'ME-EPD') {
|
if (target.tagName === 'ME-EPD') {
|
||||||
if (e.ctrlKey || e.metaKey) {
|
if (e.ctrlKey || e.metaKey) {
|
||||||
mind.expandNodeAll((target as Expander).previousSibling)
|
mind.expandNodeAll((target as Expander).previousSibling)
|
||||||
} else {
|
} else {
|
||||||
mind.expandNode((target as Expander).previousSibling)
|
mind.expandNode((target as Expander).previousSibling)
|
||||||
}
|
}
|
||||||
} else if (target.tagName === 'ME-TPC' && mind.currentNodes.length > 1) {
|
} else if (target.tagName === 'ME-TPC' && mind.currentNodes.length > 1) {
|
||||||
// This is a bit complex, intertwined with selection and nodeDraggable
|
// This is a bit complex, intertwined with selection and nodeDraggable
|
||||||
// The main conflict is between multi-node dragging and selecting a single node when multiple nodes are already selected
|
// The main conflict is between multi-node dragging and selecting a single node when multiple nodes are already selected
|
||||||
mind.selectNode(target as Topic)
|
mind.selectNode(target as Topic)
|
||||||
} else if (!mind.editable) {
|
} else if (!mind.editable) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const trySvg = target.parentElement?.parentElement as unknown as SVGElement
|
const trySvg = target.parentElement?.parentElement as unknown as SVGElement
|
||||||
if (trySvg.getAttribute('class') === 'topiclinks') {
|
if (trySvg.getAttribute('class') === 'topiclinks') {
|
||||||
mind.selectArrow(target.parentElement as unknown as CustomSvg)
|
mind.selectArrow(target.parentElement as unknown as CustomSvg)
|
||||||
} else if (trySvg.getAttribute('class') === 'summary') {
|
} else if (trySvg.getAttribute('class') === 'summary') {
|
||||||
mind.selectSummary(target.parentElement as unknown as SummarySvgGroup)
|
mind.selectSummary(target.parentElement as unknown as SummarySvgGroup)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDblClick = (e: MouseEvent) => {
|
const handleDblClick = (e: MouseEvent) => {
|
||||||
const target = e.target as HTMLElement
|
const target = e.target as HTMLElement
|
||||||
|
|
||||||
// 检查是否双击了图片
|
// 检查是否双击了图片
|
||||||
if (target.tagName === 'IMG') {
|
if (target.tagName === 'IMG') {
|
||||||
const img = target as HTMLImageElement
|
const img = target as HTMLImageElement
|
||||||
const imageUrl = img.src
|
const imageUrl = img.src
|
||||||
const altText = img.alt || img.title || ''
|
const altText = img.alt || img.title || ''
|
||||||
|
|
||||||
console.log('🖼️ 双击图片节点,准备预览:', { imageUrl, altText })
|
console.log('🖼️ 双击图片节点,准备编辑:', { imageUrl, altText })
|
||||||
|
|
||||||
// 触发图片预览事件
|
// 触发图片编辑事件
|
||||||
mind.bus.fire('showImagePreview', imageUrl, altText)
|
mind.bus.fire('showImageEditor', imageUrl, altText, img)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否双击了包含图片的节点
|
// 检查是否双击了包含图片的节点
|
||||||
if (isTopic(target)) {
|
if (isTopic(target)) {
|
||||||
const topic = target as Topic
|
const topic = target as Topic
|
||||||
|
|
||||||
// 检查节点是否有图片
|
// 检查节点是否有图片
|
||||||
if (topic.nodeObj?.image) {
|
if (topic.nodeObj?.image) {
|
||||||
const imageUrl = typeof topic.nodeObj.image === 'string' ? topic.nodeObj.image : topic.nodeObj.image.url
|
const imageUrl = typeof topic.nodeObj.image === 'string' ? topic.nodeObj.image : topic.nodeObj.image.url
|
||||||
console.log('🖼️ 双击包含图片的节点,准备预览:', imageUrl)
|
console.log('🖼️ 双击包含图片的节点,准备预览:', imageUrl)
|
||||||
mind.bus.fire('showImagePreview', imageUrl, topic.nodeObj.topic || '')
|
mind.bus.fire('showImagePreview', imageUrl, topic.nodeObj.topic || '')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查节点内容中是否包含图片
|
// 检查节点内容中是否包含图片
|
||||||
const imgInContent = topic.querySelector('img')
|
const imgInContent = topic.querySelector('img')
|
||||||
if (imgInContent) {
|
if (imgInContent) {
|
||||||
const imageUrl = imgInContent.src
|
const imageUrl = imgInContent.src
|
||||||
const altText = imgInContent.alt || imgInContent.title || topic.nodeObj?.topic || ''
|
const altText = imgInContent.alt || imgInContent.title || topic.nodeObj?.topic || ''
|
||||||
|
|
||||||
console.log('🖼️ 双击包含HTML图片的节点,准备预览:', { imageUrl, altText })
|
console.log('🖼️ 双击包含HTML图片的节点,准备编辑:', { imageUrl, altText })
|
||||||
mind.bus.fire('showImagePreview', imageUrl, altText)
|
mind.bus.fire('showImageEditor', imageUrl, altText, imgInContent)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否是表格节点
|
// 检查是否是表格节点
|
||||||
if (topic.nodeObj?.dangerouslySetInnerHTML && topic.innerHTML.includes('<table')) {
|
if (topic.nodeObj?.dangerouslySetInnerHTML && topic.innerHTML.includes('<table')) {
|
||||||
console.log('📊 双击表格节点,准备编辑:', topic.nodeObj.topic)
|
console.log('📊 双击表格节点,准备编辑:', topic.nodeObj.topic)
|
||||||
if (mind.editable) {
|
if (mind.editable) {
|
||||||
mind.beginEdit(target)
|
mind.beginEdit(target)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果没有图片或表格,则进入编辑模式
|
// 如果没有图片或表格,则进入编辑模式
|
||||||
if (mind.editable) {
|
if (mind.editable) {
|
||||||
mind.beginEdit(target)
|
mind.beginEdit(target)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理其他双击事件
|
// 处理其他双击事件
|
||||||
if (mind.editable) {
|
if (mind.editable) {
|
||||||
const trySvg = target.parentElement?.parentElement as unknown as SVGElement
|
const trySvg = target.parentElement?.parentElement as unknown as SVGElement
|
||||||
if (trySvg.getAttribute('class') === 'topiclinks') {
|
if (trySvg.getAttribute('class') === 'topiclinks') {
|
||||||
mind.editArrowLabel(target.parentElement as unknown as CustomSvg)
|
mind.editArrowLabel(target.parentElement as unknown as CustomSvg)
|
||||||
} else if (trySvg.getAttribute('class') === 'summary') {
|
} else if (trySvg.getAttribute('class') === 'summary') {
|
||||||
mind.editSummary(target.parentElement as unknown as SummarySvgGroup)
|
mind.editSummary(target.parentElement as unknown as SummarySvgGroup)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let lastTap = 0
|
let lastTap = 0
|
||||||
const handleTouchDblClick = (e: PointerEvent) => {
|
const handleTouchDblClick = (e: PointerEvent) => {
|
||||||
if (e.pointerType === 'mouse') return
|
if (e.pointerType === 'mouse') return
|
||||||
const currentTime = new Date().getTime()
|
const currentTime = new Date().getTime()
|
||||||
const tapLength = currentTime - lastTap
|
const tapLength = currentTime - lastTap
|
||||||
console.log('tapLength', tapLength)
|
console.log('tapLength', tapLength)
|
||||||
if (tapLength < 300 && tapLength > 0) {
|
if (tapLength < 300 && tapLength > 0) {
|
||||||
handleDblClick(e)
|
handleDblClick(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
lastTap = currentTime
|
lastTap = currentTime
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlePointerDown = (e: PointerEvent) => {
|
const handlePointerDown = (e: PointerEvent) => {
|
||||||
dragMoveHelper.moved = false
|
dragMoveHelper.moved = false
|
||||||
const mouseMoveButton = mind.mouseSelectionButton === 0 ? 2 : 0
|
const mouseMoveButton = mind.mouseSelectionButton === 0 ? 2 : 0
|
||||||
if (e.button !== mouseMoveButton && e.pointerType === 'mouse') return
|
if (e.button !== mouseMoveButton && e.pointerType === 'mouse') return
|
||||||
|
|
||||||
// Store initial position for movement calculation
|
// Store initial position for movement calculation
|
||||||
dragMoveHelper.x = e.clientX
|
dragMoveHelper.x = e.clientX
|
||||||
dragMoveHelper.y = e.clientY
|
dragMoveHelper.y = e.clientY
|
||||||
|
|
||||||
const target = e.target as HTMLElement
|
const target = e.target as HTMLElement
|
||||||
if (target.className === 'circle') return
|
if (target.className === 'circle') return
|
||||||
if (target.contentEditable !== 'plaintext-only') {
|
if (target.contentEditable !== 'plaintext-only') {
|
||||||
dragMoveHelper.mousedown = true
|
dragMoveHelper.mousedown = true
|
||||||
// Capture pointer to ensure we receive all pointer events even if pointer moves outside the element
|
// Capture pointer to ensure we receive all pointer events even if pointer moves outside the element
|
||||||
target.setPointerCapture(e.pointerId)
|
target.setPointerCapture(e.pointerId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlePointerMove = (e: PointerEvent) => {
|
const handlePointerMove = (e: PointerEvent) => {
|
||||||
// click trigger pointermove in windows chrome
|
// click trigger pointermove in windows chrome
|
||||||
if ((e.target as HTMLElement).contentEditable !== 'plaintext-only') {
|
if ((e.target as HTMLElement).contentEditable !== 'plaintext-only') {
|
||||||
// drag and move the map
|
// drag and move the map
|
||||||
// Calculate movement delta manually since pointer events don't have movementX/Y
|
// Calculate movement delta manually since pointer events don't have movementX/Y
|
||||||
const movementX = e.clientX - dragMoveHelper.x
|
const movementX = e.clientX - dragMoveHelper.x
|
||||||
const movementY = e.clientY - dragMoveHelper.y
|
const movementY = e.clientY - dragMoveHelper.y
|
||||||
|
|
||||||
dragMoveHelper.onMove(movementX, movementY)
|
dragMoveHelper.onMove(movementX, movementY)
|
||||||
}
|
}
|
||||||
|
|
||||||
dragMoveHelper.x = e.clientX
|
dragMoveHelper.x = e.clientX
|
||||||
dragMoveHelper.y = e.clientY
|
dragMoveHelper.y = e.clientY
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlePointerUp = (e: PointerEvent) => {
|
const handlePointerUp = (e: PointerEvent) => {
|
||||||
const mouseMoveButton = mind.mouseSelectionButton === 0 ? 2 : 0
|
const mouseMoveButton = mind.mouseSelectionButton === 0 ? 2 : 0
|
||||||
if (e.button !== mouseMoveButton && e.pointerType === 'mouse') return
|
if (e.button !== mouseMoveButton && e.pointerType === 'mouse') return
|
||||||
const target = e.target as HTMLElement
|
const target = e.target as HTMLElement
|
||||||
// Release pointer capture
|
// Release pointer capture
|
||||||
if (target.hasPointerCapture && target.hasPointerCapture(e.pointerId)) {
|
if (target.hasPointerCapture && target.hasPointerCapture(e.pointerId)) {
|
||||||
target.releasePointerCapture(e.pointerId)
|
target.releasePointerCapture(e.pointerId)
|
||||||
}
|
}
|
||||||
dragMoveHelper.clear()
|
dragMoveHelper.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleContextMenu = (e: MouseEvent) => {
|
const handleContextMenu = (e: MouseEvent) => {
|
||||||
console.log('handleContextMenu', e)
|
console.log('handleContextMenu', e)
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
// Only handle right-click for context menu
|
// Only handle right-click for context menu
|
||||||
if (e.button !== 2) return
|
if (e.button !== 2) return
|
||||||
if (!mind.editable) return
|
if (!mind.editable) return
|
||||||
const target = e.target as HTMLElement
|
const target = e.target as HTMLElement
|
||||||
if (isTopic(target) && !target.classList.contains('selected')) {
|
if (isTopic(target) && !target.classList.contains('selected')) {
|
||||||
mind.selectNode(target)
|
mind.selectNode(target)
|
||||||
}
|
}
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// delay to avoid conflict with click event on Mac
|
// delay to avoid conflict with click event on Mac
|
||||||
if (mind.dragMoveHelper.moved) return
|
if (mind.dragMoveHelper.moved) return
|
||||||
mind.bus.fire('showContextMenu', e)
|
mind.bus.fire('showContextMenu', e)
|
||||||
}, 200)
|
}, 200)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleWheel = (e: WheelEvent) => {
|
const handleWheel = (e: WheelEvent) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (e.ctrlKey || e.metaKey) {
|
if (e.ctrlKey || e.metaKey) {
|
||||||
if (e.deltaY < 0) handleZoom(mind, 'in', mind.dragMoveHelper)
|
if (e.deltaY < 0) handleZoom(mind, 'in', mind.dragMoveHelper)
|
||||||
else if (mind.scaleVal - mind.scaleSensitivity > 0) handleZoom(mind, 'out', mind.dragMoveHelper)
|
else if (mind.scaleVal - mind.scaleSensitivity > 0) handleZoom(mind, 'out', mind.dragMoveHelper)
|
||||||
} else if (e.shiftKey) {
|
} else if (e.shiftKey) {
|
||||||
mind.move(-e.deltaY, 0)
|
mind.move(-e.deltaY, 0)
|
||||||
} else {
|
} else {
|
||||||
mind.move(-e.deltaX, -e.deltaY)
|
mind.move(-e.deltaX, -e.deltaY)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { container } = mind
|
const { container } = mind
|
||||||
const off = on([
|
const off = on([
|
||||||
{ dom: container, evt: 'pointerdown', func: handlePointerDown },
|
{ dom: container, evt: 'pointerdown', func: handlePointerDown },
|
||||||
{ dom: container, evt: 'pointermove', func: handlePointerMove },
|
{ dom: container, evt: 'pointermove', func: handlePointerMove },
|
||||||
{ dom: container, evt: 'pointerup', func: handlePointerUp },
|
{ dom: container, evt: 'pointerup', func: handlePointerUp },
|
||||||
{ dom: container, evt: 'pointerup', func: handleTouchDblClick },
|
{ dom: container, evt: 'pointerup', func: handleTouchDblClick },
|
||||||
{ dom: container, evt: 'click', func: handleClick },
|
{ dom: container, evt: 'click', func: handleClick },
|
||||||
{ dom: container, evt: 'dblclick', func: handleDblClick },
|
{ dom: container, evt: 'dblclick', func: handleDblClick },
|
||||||
{ dom: container, evt: 'contextmenu', func: handleContextMenu },
|
{ dom: container, evt: 'contextmenu', func: handleContextMenu },
|
||||||
{ dom: container, evt: 'wheel', func: typeof mind.handleWheel === 'function' ? mind.handleWheel : handleWheel },
|
{ dom: container, evt: 'wheel', func: typeof mind.handleWheel === 'function' ? mind.handleWheel : handleWheel },
|
||||||
])
|
])
|
||||||
return off
|
return off
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ export default function (mind: MindElixirInstance, option: true | ContextMenuOpt
|
||||||
const linkBidirectional = createLi('cm-link-bidirectional', lang.linkBidirectional, '')
|
const linkBidirectional = createLi('cm-link-bidirectional', lang.linkBidirectional, '')
|
||||||
const summary = createLi('cm-summary', lang.summary, '')
|
const summary = createLi('cm-summary', lang.summary, '')
|
||||||
const imagePreview = createLi('cm-image-preview', '预览图片', '')
|
const imagePreview = createLi('cm-image-preview', '预览图片', '')
|
||||||
|
const imageEdit = createLi('cm-image-edit', '编辑图片', '')
|
||||||
|
|
||||||
const menuUl = document.createElement('ul')
|
const menuUl = document.createElement('ul')
|
||||||
menuUl.className = 'menu-list'
|
menuUl.className = 'menu-list'
|
||||||
|
|
@ -63,6 +64,8 @@ export default function (mind: MindElixirInstance, option: true | ContextMenuOpt
|
||||||
menuUl.appendChild(up)
|
menuUl.appendChild(up)
|
||||||
menuUl.appendChild(down)
|
menuUl.appendChild(down)
|
||||||
menuUl.appendChild(summary)
|
menuUl.appendChild(summary)
|
||||||
|
menuUl.appendChild(imagePreview)
|
||||||
|
menuUl.appendChild(imageEdit)
|
||||||
if (option.link) {
|
if (option.link) {
|
||||||
menuUl.appendChild(link)
|
menuUl.appendChild(link)
|
||||||
menuUl.appendChild(linkBidirectional)
|
menuUl.appendChild(linkBidirectional)
|
||||||
|
|
@ -102,8 +105,11 @@ export default function (mind: MindElixirInstance, option: true | ContextMenuOpt
|
||||||
if (hasImage) {
|
if (hasImage) {
|
||||||
imagePreview.style.display = 'block'
|
imagePreview.style.display = 'block'
|
||||||
imagePreview.className = ''
|
imagePreview.className = ''
|
||||||
|
imageEdit.style.display = 'block'
|
||||||
|
imageEdit.className = ''
|
||||||
} else {
|
} else {
|
||||||
imagePreview.style.display = 'none'
|
imagePreview.style.display = 'none'
|
||||||
|
imageEdit.style.display = 'none'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isRoot) {
|
if (isRoot) {
|
||||||
|
|
@ -246,6 +252,29 @@ export default function (mind: MindElixirInstance, option: true | ContextMenuOpt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
imageEdit.onclick = () => {
|
||||||
|
menuContainer.hidden = true
|
||||||
|
const target = mind.currentNode as Topic
|
||||||
|
if (target) {
|
||||||
|
// 检查节点是否有图片
|
||||||
|
if (target.nodeObj?.image) {
|
||||||
|
const imageUrl = typeof target.nodeObj.image === 'string' ? target.nodeObj.image : target.nodeObj.image.url
|
||||||
|
console.log('🖼️ 右键菜单编辑图片:', imageUrl)
|
||||||
|
mind.bus.fire('showImageEditor', imageUrl, target.nodeObj.topic || '', null)
|
||||||
|
} else {
|
||||||
|
// 检查节点内容中是否包含图片
|
||||||
|
const imgInContent = target.querySelector('img')
|
||||||
|
if (imgInContent) {
|
||||||
|
const imageUrl = imgInContent.src
|
||||||
|
const altText = imgInContent.alt || imgInContent.title || target.nodeObj?.topic || ''
|
||||||
|
|
||||||
|
console.log('🖼️ 右键菜单编辑HTML图片:', { imageUrl, altText })
|
||||||
|
mind.bus.fire('showImageEditor', imageUrl, altText, imgInContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return () => {
|
return () => {
|
||||||
// maybe useful?
|
// maybe useful?
|
||||||
add_child.onclick = null
|
add_child.onclick = null
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,7 @@ export type EventMap = {
|
||||||
updateArrowDelta: (arrow: Arrow) => void
|
updateArrowDelta: (arrow: Arrow) => void
|
||||||
showContextMenu: (e: MouseEvent) => void
|
showContextMenu: (e: MouseEvent) => void
|
||||||
showImagePreview: (imageUrl: string, altText?: string) => void
|
showImagePreview: (imageUrl: string, altText?: string) => void
|
||||||
|
showImageEditor: (imageUrl: string, altText?: string, imgElement?: HTMLImageElement | null) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createBus<T extends Record<string, (...args: any[]) => void> = EventMap>() {
|
export function createBus<T extends Record<string, (...args: any[]) => void> = EventMap>() {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue