From e3fedcbf0ff6f3a4a9392ebfd4fce1002bd9a282 Mon Sep 17 00:00:00 2001 From: lixinran Date: Sat, 11 Oct 2025 14:19:41 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20=E5=AE=9E=E7=8E=B0=E5=9B=BE?= =?UTF-8?q?=E7=89=87=E7=BC=96=E8=BE=91=E5=8A=9F=E8=83=BD=EF=BC=8C=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E5=9B=BE=E7=89=87=E4=BA=A4=E4=BA=92=E4=BD=93=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 核心功能: - 双击图片 → 进入编辑模式(替换图片) - 右键图片 → 显示菜单(预览/编辑选项) - 图片编辑器支持文件选择和替换 - 支持HTML图片和MindElixir原生图片两种类型 技术实现: - 修改mouse.ts,双击图片触发showImageEditor事件 - 扩展contextMenu.ts,添加图片编辑菜单项 - 更新pubsub.ts,添加showImageEditor事件类型 - 在MindMap.vue中实现图片编辑器UI和逻辑 - 添加文件选择、格式验证、大小限制(5MB) - 支持JPG/PNG/GIF格式 UI设计: - 紫色主题编辑器(与品牌色一致) - 当前图片预览 + 文件选择界面 - 响应式设计,支持大图片显示 - 优雅的模态框和交互体验 --- frontend/src/components/MindMap.vue | 263 +++++++++++ frontend/src/lib/mind-elixir/src/mouse.ts | 424 +++++++++--------- .../lib/mind-elixir/src/plugin/contextMenu.ts | 29 ++ .../src/lib/mind-elixir/src/utils/pubsub.ts | 1 + 4 files changed, 505 insertions(+), 212 deletions(-) diff --git a/frontend/src/components/MindMap.vue b/frontend/src/components/MindMap.vue index 1aad384..615c4e6 100644 --- a/frontend/src/components/MindMap.vue +++ b/frontend/src/components/MindMap.vue @@ -164,6 +164,40 @@ + +
+
+
+ {{ imageEditorTitle }} + +
+
+
+

当前图片:

+ +
+
+

替换图片:

+
+ + +
+

支持 JPG、PNG、GIF 格式,最大 5MB

+
+
+ +
+
@@ -256,6 +290,12 @@ const imagePreviewTitle = ref(''); const imagePreviewLoading = ref(false); 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 = ''; }; +// 图片编辑器相关函数 +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 = () => { console.log('✅ 模态框图片加载成功'); // 图片已经在预加载阶段处理了状态,这里只是确认 @@ -636,6 +739,12 @@ const loadMindmapData = async (data, keepPosition = false, shouldCenterRoot = tr openImagePreview(imageUrl, altText); }); + // 添加图片编辑事件监听器 + mindElixir.value.bus.addListener('showImageEditor', (imageUrl, altText, imgElement) => { + console.log('🖼️ 收到图片编辑事件:', { imageUrl, altText }); + openImageEditor(imageUrl, altText, imgElement); + }); + // Mind Elixir现在会自动使用markdown解析器渲染内容 } else { @@ -724,6 +833,12 @@ const loadMindmapData = async (data, keepPosition = false, shouldCenterRoot = tr openImagePreview(imageUrl, altText); }); + // 添加图片编辑事件监听器 + mindElixir.value.bus.addListener('showImageEditor', (imageUrl, altText, imgElement) => { + console.log('🖼️ 收到图片编辑事件(延迟创建):', { imageUrl, altText }); + openImageEditor(imageUrl, altText, imgElement); + }); + // 延迟执行后续操作 setTimeout(() => { // Mind Elixir现在会自动使用markdown解析器渲染内容 @@ -4881,5 +4996,153 @@ const updateMindMapRealtime = async (data, title) => { 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; +} + diff --git a/frontend/src/lib/mind-elixir/src/mouse.ts b/frontend/src/lib/mind-elixir/src/mouse.ts index 52662a0..d6fa452 100644 --- a/frontend/src/lib/mind-elixir/src/mouse.ts +++ b/frontend/src/lib/mind-elixir/src/mouse.ts @@ -1,212 +1,212 @@ -import { handleZoom } from './plugin/keypress' -import type { SummarySvgGroup } from './summary' -import type { Expander, CustomSvg, Topic } from './types/dom' -import type { MindElixirInstance } from './types/index' -import { isTopic, on } from './utils' - -export default function (mind: MindElixirInstance) { - const { dragMoveHelper } = mind - - const handleClick = (e: MouseEvent) => { - console.log('handleClick', e) - // Only handle primary button clicks - if (e.button !== 0) return - if (mind.helper1?.moved) { - mind.helper1.clear() - return - } - if (mind.helper2?.moved) { - mind.helper2.clear() - return - } - if (dragMoveHelper.moved) { - dragMoveHelper.clear() - return - } - const target = e.target as HTMLElement - if (target.tagName === 'ME-EPD') { - if (e.ctrlKey || e.metaKey) { - mind.expandNodeAll((target as Expander).previousSibling) - } else { - mind.expandNode((target as Expander).previousSibling) - } - } else if (target.tagName === 'ME-TPC' && mind.currentNodes.length > 1) { - // 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 - mind.selectNode(target as Topic) - } else if (!mind.editable) { - return - } - const trySvg = target.parentElement?.parentElement as unknown as SVGElement - if (trySvg.getAttribute('class') === 'topiclinks') { - mind.selectArrow(target.parentElement as unknown as CustomSvg) - } else if (trySvg.getAttribute('class') === 'summary') { - mind.selectSummary(target.parentElement as unknown as SummarySvgGroup) - } - } - - const handleDblClick = (e: MouseEvent) => { - const target = e.target as HTMLElement - - // 检查是否双击了图片 - if (target.tagName === 'IMG') { - const img = target as HTMLImageElement - const imageUrl = img.src - const altText = img.alt || img.title || '' - - console.log('🖼️ 双击图片节点,准备预览:', { imageUrl, altText }) - - // 触发图片预览事件 - mind.bus.fire('showImagePreview', imageUrl, altText) - return - } - - // 检查是否双击了包含图片的节点 - if (isTopic(target)) { - const topic = target as Topic - - // 检查节点是否有图片 - if (topic.nodeObj?.image) { - const imageUrl = typeof topic.nodeObj.image === 'string' ? topic.nodeObj.image : topic.nodeObj.image.url - console.log('🖼️ 双击包含图片的节点,准备预览:', imageUrl) - mind.bus.fire('showImagePreview', imageUrl, topic.nodeObj.topic || '') - return - } - - // 检查节点内容中是否包含图片 - const imgInContent = topic.querySelector('img') - if (imgInContent) { - const imageUrl = imgInContent.src - const altText = imgInContent.alt || imgInContent.title || topic.nodeObj?.topic || '' - - console.log('🖼️ 双击包含HTML图片的节点,准备预览:', { imageUrl, altText }) - mind.bus.fire('showImagePreview', imageUrl, altText) - return - } - - // 检查是否是表格节点 - if (topic.nodeObj?.dangerouslySetInnerHTML && topic.innerHTML.includes(' { - if (e.pointerType === 'mouse') return - const currentTime = new Date().getTime() - const tapLength = currentTime - lastTap - console.log('tapLength', tapLength) - if (tapLength < 300 && tapLength > 0) { - handleDblClick(e) - } - - lastTap = currentTime - } - - const handlePointerDown = (e: PointerEvent) => { - dragMoveHelper.moved = false - const mouseMoveButton = mind.mouseSelectionButton === 0 ? 2 : 0 - if (e.button !== mouseMoveButton && e.pointerType === 'mouse') return - - // Store initial position for movement calculation - dragMoveHelper.x = e.clientX - dragMoveHelper.y = e.clientY - - const target = e.target as HTMLElement - if (target.className === 'circle') return - if (target.contentEditable !== 'plaintext-only') { - dragMoveHelper.mousedown = true - // Capture pointer to ensure we receive all pointer events even if pointer moves outside the element - target.setPointerCapture(e.pointerId) - } - } - - const handlePointerMove = (e: PointerEvent) => { - // click trigger pointermove in windows chrome - if ((e.target as HTMLElement).contentEditable !== 'plaintext-only') { - // drag and move the map - // Calculate movement delta manually since pointer events don't have movementX/Y - const movementX = e.clientX - dragMoveHelper.x - const movementY = e.clientY - dragMoveHelper.y - - dragMoveHelper.onMove(movementX, movementY) - } - - dragMoveHelper.x = e.clientX - dragMoveHelper.y = e.clientY - } - - const handlePointerUp = (e: PointerEvent) => { - const mouseMoveButton = mind.mouseSelectionButton === 0 ? 2 : 0 - if (e.button !== mouseMoveButton && e.pointerType === 'mouse') return - const target = e.target as HTMLElement - // Release pointer capture - if (target.hasPointerCapture && target.hasPointerCapture(e.pointerId)) { - target.releasePointerCapture(e.pointerId) - } - dragMoveHelper.clear() - } - - const handleContextMenu = (e: MouseEvent) => { - console.log('handleContextMenu', e) - e.preventDefault() - // Only handle right-click for context menu - if (e.button !== 2) return - if (!mind.editable) return - const target = e.target as HTMLElement - if (isTopic(target) && !target.classList.contains('selected')) { - mind.selectNode(target) - } - setTimeout(() => { - // delay to avoid conflict with click event on Mac - if (mind.dragMoveHelper.moved) return - mind.bus.fire('showContextMenu', e) - }, 200) - } - - const handleWheel = (e: WheelEvent) => { - e.stopPropagation() - e.preventDefault() - if (e.ctrlKey || e.metaKey) { - if (e.deltaY < 0) handleZoom(mind, 'in', mind.dragMoveHelper) - else if (mind.scaleVal - mind.scaleSensitivity > 0) handleZoom(mind, 'out', mind.dragMoveHelper) - } else if (e.shiftKey) { - mind.move(-e.deltaY, 0) - } else { - mind.move(-e.deltaX, -e.deltaY) - } - } - - const { container } = mind - const off = on([ - { dom: container, evt: 'pointerdown', func: handlePointerDown }, - { dom: container, evt: 'pointermove', func: handlePointerMove }, - { dom: container, evt: 'pointerup', func: handlePointerUp }, - { dom: container, evt: 'pointerup', func: handleTouchDblClick }, - { dom: container, evt: 'click', func: handleClick }, - { dom: container, evt: 'dblclick', func: handleDblClick }, - { dom: container, evt: 'contextmenu', func: handleContextMenu }, - { dom: container, evt: 'wheel', func: typeof mind.handleWheel === 'function' ? mind.handleWheel : handleWheel }, - ]) - return off -} +import { handleZoom } from './plugin/keypress' +import type { SummarySvgGroup } from './summary' +import type { Expander, CustomSvg, Topic } from './types/dom' +import type { MindElixirInstance } from './types/index' +import { isTopic, on } from './utils' + +export default function (mind: MindElixirInstance) { + const { dragMoveHelper } = mind + + const handleClick = (e: MouseEvent) => { + console.log('handleClick', e) + // Only handle primary button clicks + if (e.button !== 0) return + if (mind.helper1?.moved) { + mind.helper1.clear() + return + } + if (mind.helper2?.moved) { + mind.helper2.clear() + return + } + if (dragMoveHelper.moved) { + dragMoveHelper.clear() + return + } + const target = e.target as HTMLElement + if (target.tagName === 'ME-EPD') { + if (e.ctrlKey || e.metaKey) { + mind.expandNodeAll((target as Expander).previousSibling) + } else { + mind.expandNode((target as Expander).previousSibling) + } + } else if (target.tagName === 'ME-TPC' && mind.currentNodes.length > 1) { + // 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 + mind.selectNode(target as Topic) + } else if (!mind.editable) { + return + } + const trySvg = target.parentElement?.parentElement as unknown as SVGElement + if (trySvg.getAttribute('class') === 'topiclinks') { + mind.selectArrow(target.parentElement as unknown as CustomSvg) + } else if (trySvg.getAttribute('class') === 'summary') { + mind.selectSummary(target.parentElement as unknown as SummarySvgGroup) + } + } + + const handleDblClick = (e: MouseEvent) => { + const target = e.target as HTMLElement + + // 检查是否双击了图片 + if (target.tagName === 'IMG') { + const img = target as HTMLImageElement + const imageUrl = img.src + const altText = img.alt || img.title || '' + + console.log('🖼️ 双击图片节点,准备编辑:', { imageUrl, altText }) + + // 触发图片编辑事件 + mind.bus.fire('showImageEditor', imageUrl, altText, img) + return + } + + // 检查是否双击了包含图片的节点 + if (isTopic(target)) { + const topic = target as Topic + + // 检查节点是否有图片 + if (topic.nodeObj?.image) { + const imageUrl = typeof topic.nodeObj.image === 'string' ? topic.nodeObj.image : topic.nodeObj.image.url + console.log('🖼️ 双击包含图片的节点,准备预览:', imageUrl) + mind.bus.fire('showImagePreview', imageUrl, topic.nodeObj.topic || '') + return + } + + // 检查节点内容中是否包含图片 + const imgInContent = topic.querySelector('img') + if (imgInContent) { + const imageUrl = imgInContent.src + const altText = imgInContent.alt || imgInContent.title || topic.nodeObj?.topic || '' + + console.log('🖼️ 双击包含HTML图片的节点,准备编辑:', { imageUrl, altText }) + mind.bus.fire('showImageEditor', imageUrl, altText, imgInContent) + return + } + + // 检查是否是表格节点 + if (topic.nodeObj?.dangerouslySetInnerHTML && topic.innerHTML.includes(' { + if (e.pointerType === 'mouse') return + const currentTime = new Date().getTime() + const tapLength = currentTime - lastTap + console.log('tapLength', tapLength) + if (tapLength < 300 && tapLength > 0) { + handleDblClick(e) + } + + lastTap = currentTime + } + + const handlePointerDown = (e: PointerEvent) => { + dragMoveHelper.moved = false + const mouseMoveButton = mind.mouseSelectionButton === 0 ? 2 : 0 + if (e.button !== mouseMoveButton && e.pointerType === 'mouse') return + + // Store initial position for movement calculation + dragMoveHelper.x = e.clientX + dragMoveHelper.y = e.clientY + + const target = e.target as HTMLElement + if (target.className === 'circle') return + if (target.contentEditable !== 'plaintext-only') { + dragMoveHelper.mousedown = true + // Capture pointer to ensure we receive all pointer events even if pointer moves outside the element + target.setPointerCapture(e.pointerId) + } + } + + const handlePointerMove = (e: PointerEvent) => { + // click trigger pointermove in windows chrome + if ((e.target as HTMLElement).contentEditable !== 'plaintext-only') { + // drag and move the map + // Calculate movement delta manually since pointer events don't have movementX/Y + const movementX = e.clientX - dragMoveHelper.x + const movementY = e.clientY - dragMoveHelper.y + + dragMoveHelper.onMove(movementX, movementY) + } + + dragMoveHelper.x = e.clientX + dragMoveHelper.y = e.clientY + } + + const handlePointerUp = (e: PointerEvent) => { + const mouseMoveButton = mind.mouseSelectionButton === 0 ? 2 : 0 + if (e.button !== mouseMoveButton && e.pointerType === 'mouse') return + const target = e.target as HTMLElement + // Release pointer capture + if (target.hasPointerCapture && target.hasPointerCapture(e.pointerId)) { + target.releasePointerCapture(e.pointerId) + } + dragMoveHelper.clear() + } + + const handleContextMenu = (e: MouseEvent) => { + console.log('handleContextMenu', e) + e.preventDefault() + // Only handle right-click for context menu + if (e.button !== 2) return + if (!mind.editable) return + const target = e.target as HTMLElement + if (isTopic(target) && !target.classList.contains('selected')) { + mind.selectNode(target) + } + setTimeout(() => { + // delay to avoid conflict with click event on Mac + if (mind.dragMoveHelper.moved) return + mind.bus.fire('showContextMenu', e) + }, 200) + } + + const handleWheel = (e: WheelEvent) => { + e.stopPropagation() + e.preventDefault() + if (e.ctrlKey || e.metaKey) { + if (e.deltaY < 0) handleZoom(mind, 'in', mind.dragMoveHelper) + else if (mind.scaleVal - mind.scaleSensitivity > 0) handleZoom(mind, 'out', mind.dragMoveHelper) + } else if (e.shiftKey) { + mind.move(-e.deltaY, 0) + } else { + mind.move(-e.deltaX, -e.deltaY) + } + } + + const { container } = mind + const off = on([ + { dom: container, evt: 'pointerdown', func: handlePointerDown }, + { dom: container, evt: 'pointermove', func: handlePointerMove }, + { dom: container, evt: 'pointerup', func: handlePointerUp }, + { dom: container, evt: 'pointerup', func: handleTouchDblClick }, + { dom: container, evt: 'click', func: handleClick }, + { dom: container, evt: 'dblclick', func: handleDblClick }, + { dom: container, evt: 'contextmenu', func: handleContextMenu }, + { dom: container, evt: 'wheel', func: typeof mind.handleWheel === 'function' ? mind.handleWheel : handleWheel }, + ]) + return off +} diff --git a/frontend/src/lib/mind-elixir/src/plugin/contextMenu.ts b/frontend/src/lib/mind-elixir/src/plugin/contextMenu.ts index e9f0779..efcfb61 100644 --- a/frontend/src/lib/mind-elixir/src/plugin/contextMenu.ts +++ b/frontend/src/lib/mind-elixir/src/plugin/contextMenu.ts @@ -49,6 +49,7 @@ export default function (mind: MindElixirInstance, option: true | ContextMenuOpt const linkBidirectional = createLi('cm-link-bidirectional', lang.linkBidirectional, '') const summary = createLi('cm-summary', lang.summary, '') const imagePreview = createLi('cm-image-preview', '预览图片', '') + const imageEdit = createLi('cm-image-edit', '编辑图片', '') const menuUl = document.createElement('ul') menuUl.className = 'menu-list' @@ -63,6 +64,8 @@ export default function (mind: MindElixirInstance, option: true | ContextMenuOpt menuUl.appendChild(up) menuUl.appendChild(down) menuUl.appendChild(summary) + menuUl.appendChild(imagePreview) + menuUl.appendChild(imageEdit) if (option.link) { menuUl.appendChild(link) menuUl.appendChild(linkBidirectional) @@ -102,8 +105,11 @@ export default function (mind: MindElixirInstance, option: true | ContextMenuOpt if (hasImage) { imagePreview.style.display = 'block' imagePreview.className = '' + imageEdit.style.display = 'block' + imageEdit.className = '' } else { imagePreview.style.display = 'none' + imageEdit.style.display = 'none' } 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 () => { // maybe useful? add_child.onclick = null diff --git a/frontend/src/lib/mind-elixir/src/utils/pubsub.ts b/frontend/src/lib/mind-elixir/src/utils/pubsub.ts index 7652a22..5f31d43 100644 --- a/frontend/src/lib/mind-elixir/src/utils/pubsub.ts +++ b/frontend/src/lib/mind-elixir/src/utils/pubsub.ts @@ -89,6 +89,7 @@ export type EventMap = { updateArrowDelta: (arrow: Arrow) => void showContextMenu: (e: MouseEvent) => void showImagePreview: (imageUrl: string, altText?: string) => void + showImageEditor: (imageUrl: string, altText?: string, imgElement?: HTMLImageElement | null) => void } export function createBus void> = EventMap>() {