✨ 优化表格编辑器UI和用户体验
改进内容: - 使用fixed定位的模态框,居中显示 - 添加半透明遮罩层,突出编辑器 - 增大编辑器尺寸(600x400px),更舒适的编辑空间 - 设置明显的光标颜色(caret-color: #007bff) - 添加编辑器标题和快捷键提示 - 优化按钮样式,添加hover效果 - 设置textarea背景色(#fafafa),增强可见性 - 禁用拼写检查和自动完成 - 延迟聚焦确保光标可见 - 点击遮罩层可关闭编辑器
This commit is contained in:
parent
1c838a9aeb
commit
1f67b9ae58
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -23,8 +23,8 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script type="module" crossorigin src="/assets/index-fc166542.js"></script>
|
<script type="module" crossorigin src="/assets/index-78cb04db.js"></script>
|
||||||
<link rel="stylesheet" href="/assets/index-bafd0b52.css">
|
<link rel="stylesheet" href="/assets/index-ae3a3afb.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|
|
||||||
|
|
@ -875,8 +875,8 @@ const formatMarkdownToText = (markdown) => {
|
||||||
.replace(/`(.*?)`/g, '「$1」')
|
.replace(/`(.*?)`/g, '「$1」')
|
||||||
// 处理链接
|
// 处理链接
|
||||||
.replace(/\[([^\]]+)\]\([^)]+\)/g, '🔗 $1')
|
.replace(/\[([^\]]+)\]\([^)]+\)/g, '🔗 $1')
|
||||||
// 图片通过MindElixir原生image属性处理,保留图片占位符用于后续处理
|
// 图片现在通过markdown编辑器渲染,保留图片markdown语法
|
||||||
.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '[图片: $1]')
|
// .replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '[图片: $1]') // 注释掉,保留图片markdown语法
|
||||||
// 处理换行 - 优化换行处理,避免错位
|
// 处理换行 - 优化换行处理,避免错位
|
||||||
.replace(/\n\n/g, '\n')
|
.replace(/\n\n/g, '\n')
|
||||||
.replace(/\n/g, '\n ')
|
.replace(/\n/g, '\n ')
|
||||||
|
|
@ -953,9 +953,10 @@ const extractImageFromContent = (content) => {
|
||||||
return images;
|
return images;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 从内容中移除图片Markdown语法
|
// 从内容中移除图片Markdown语法(现在保留,因为图片节点使用markdown渲染)
|
||||||
const removeImageMarkdown = (content) => {
|
const removeImageMarkdown = (content) => {
|
||||||
return content.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '').trim();
|
// 不再移除图片markdown语法,保留用于markdown渲染
|
||||||
|
return content.trim();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Markdown转JSON的核心逻辑 - 智能层次化版本
|
// Markdown转JSON的核心逻辑 - 智能层次化版本
|
||||||
|
|
@ -1003,17 +1004,24 @@ const markdownToJSON = (markdown) => {
|
||||||
data: {},
|
data: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
// 如果标题中有图片,使用第一个图片作为节点图片
|
// 如果标题中有图片,使用markdown渲染
|
||||||
if (titleImages.length > 0) {
|
if (titleImages.length > 0) {
|
||||||
const firstImage = titleImages[0];
|
const firstImage = titleImages[0];
|
||||||
if (firstImage.url && firstImage.url.trim() !== '') {
|
if (firstImage.url && firstImage.url.trim() !== '') {
|
||||||
node.image = {
|
// 转换图片URL为代理URL
|
||||||
url: firstImage.url,
|
let imageUrl = firstImage.url;
|
||||||
width: 200,
|
if (imageUrl.includes('cdn-mineru.openxlab.org.cn')) {
|
||||||
height: 150,
|
const urlPath = imageUrl.replace('https://cdn-mineru.openxlab.org.cn', '');
|
||||||
fit: 'contain'
|
imageUrl = `/proxy-image${urlPath}`;
|
||||||
};
|
}
|
||||||
console.log(`✅ 成功为标题节点设置图片: ${firstImage.url}`);
|
|
||||||
|
// 使用markdown渲染标题中的图片
|
||||||
|
const titleMarkdown = ``;
|
||||||
|
const htmlContent = renderMarkdownToHTML(titleMarkdown);
|
||||||
|
|
||||||
|
node.topic = ''; // 清空topic
|
||||||
|
node.dangerouslySetInnerHTML = htmlContent; // 使用dangerouslySetInnerHTML
|
||||||
|
console.log(`✅ 成功为标题节点设置图片: ${imageUrl}`);
|
||||||
} else {
|
} else {
|
||||||
console.error(`❌ 标题图片URL无效:`, firstImage);
|
console.error(`❌ 标题图片URL无效:`, firstImage);
|
||||||
}
|
}
|
||||||
|
|
@ -1094,28 +1102,28 @@ const processContentIntelligently = (content, parentNode, nodeCounter) => {
|
||||||
console.log(`🔄 转换图片URL: ${image.url} -> ${imageUrl}`);
|
console.log(`🔄 转换图片URL: ${image.url} -> ${imageUrl}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 使用markdown渲染图片节点,统一技术栈
|
||||||
|
const imageMarkdown = ``;
|
||||||
|
const htmlContent = renderMarkdownToHTML(imageMarkdown);
|
||||||
|
console.log('🖼️ 图片HTML内容:', htmlContent);
|
||||||
|
|
||||||
const imageNode = {
|
const imageNode = {
|
||||||
id: `node_${nodeCounter++}`,
|
id: `node_${nodeCounter++}`,
|
||||||
topic: image.alt || `图片 ${index + 1}`,
|
topic: '', // 清空topic,使用dangerouslySetInnerHTML
|
||||||
|
dangerouslySetInnerHTML: htmlContent, // 使用dangerouslySetInnerHTML来正确处理HTML内容
|
||||||
children: [],
|
children: [],
|
||||||
level: (parentNode.level || 0) + 1,
|
level: (parentNode.level || 0) + 1,
|
||||||
image: {
|
|
||||||
url: imageUrl,
|
|
||||||
width: 200,
|
|
||||||
height: 150,
|
|
||||||
fit: 'contain'
|
|
||||||
},
|
|
||||||
data: {}
|
data: {}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 验证图片节点数据
|
// 验证图片节点数据
|
||||||
if (!imageNode.image.url || imageNode.image.url.trim() === '') {
|
if (!imageUrl || imageUrl.trim() === '') {
|
||||||
console.error(`❌ 图片节点 ${index + 1} URL为空:`, imageNode);
|
console.error(`❌ 图片节点 ${index + 1} URL为空:`, imageNode);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
parentNode.children.push(imageNode);
|
parentNode.children.push(imageNode);
|
||||||
console.log(`✅ 成功创建图片节点: ${imageNode.topic} - ${imageUrl}`);
|
console.log(`✅ 成功创建图片节点: ${image.alt || `图片 ${index + 1}`} - ${imageUrl}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 移除图片后的内容
|
// 移除图片后的内容
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import nodeDraggable from './plugin/nodeDraggable'
|
||||||
import operationHistory from './plugin/operationHistory'
|
import operationHistory from './plugin/operationHistory'
|
||||||
import toolBar from './plugin/toolBar'
|
import toolBar from './plugin/toolBar'
|
||||||
import selection from './plugin/selection'
|
import selection from './plugin/selection'
|
||||||
import { editTopic, createWrapper, createParent, createChildren, createTopic, findEle } from './utils/dom'
|
import { editTopic, editTableNode, convertTableHTMLToMarkdown, createWrapper, createParent, createChildren, createTopic, findEle } from './utils/dom'
|
||||||
import { getObjById, generateNewObj, fillParent } from './utils/index'
|
import { getObjById, generateNewObj, fillParent } from './utils/index'
|
||||||
import { layout } from './utils/layout'
|
import { layout } from './utils/layout'
|
||||||
import { changeTheme } from './utils/theme'
|
import { changeTheme } from './utils/theme'
|
||||||
|
|
@ -15,6 +15,7 @@ import * as nodeOperation from './nodeOperation'
|
||||||
import * as arrow from './arrow'
|
import * as arrow from './arrow'
|
||||||
import * as summary from './summary'
|
import * as summary from './summary'
|
||||||
import * as exportImage from './plugin/exportImage'
|
import * as exportImage from './plugin/exportImage'
|
||||||
|
import { renderMarkdownToHTML } from '../../../utils/markdownRenderer'
|
||||||
|
|
||||||
export type OperationMap = typeof nodeOperation
|
export type OperationMap = typeof nodeOperation
|
||||||
export type Operations = keyof OperationMap
|
export type Operations = keyof OperationMap
|
||||||
|
|
@ -58,6 +59,9 @@ const methods = {
|
||||||
layout,
|
layout,
|
||||||
linkDiv,
|
linkDiv,
|
||||||
editTopic,
|
editTopic,
|
||||||
|
editTableNode,
|
||||||
|
convertTableHTMLToMarkdown,
|
||||||
|
renderMarkdownToHTML,
|
||||||
createWrapper,
|
createWrapper,
|
||||||
createParent,
|
createParent,
|
||||||
createChildren,
|
createChildren,
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,16 @@ export default function (mind: MindElixirInstance) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果没有图片,则进入编辑模式
|
// 检查是否是表格节点
|
||||||
|
if (topic.nodeObj?.dangerouslySetInnerHTML && topic.innerHTML.includes('<table')) {
|
||||||
|
console.log('📊 双击表格节点,准备编辑:', topic.nodeObj.topic)
|
||||||
|
if (mind.editable) {
|
||||||
|
mind.beginEdit(target)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有图片或表格,则进入编辑模式
|
||||||
if (mind.editable) {
|
if (mind.editable) {
|
||||||
mind.beginEdit(target)
|
mind.beginEdit(target)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -303,7 +303,18 @@ export const moveNodeAfter = function (this: MindElixirInstance, from: Topic[],
|
||||||
export const beginEdit = function (this: MindElixirInstance, el?: Topic) {
|
export const beginEdit = function (this: MindElixirInstance, el?: Topic) {
|
||||||
const nodeEle = el || this.currentNode
|
const nodeEle = el || this.currentNode
|
||||||
if (!nodeEle) return
|
if (!nodeEle) return
|
||||||
if (nodeEle.nodeObj.dangerouslySetInnerHTML) return
|
|
||||||
|
// 检查是否是表格节点
|
||||||
|
if (nodeEle.nodeObj.dangerouslySetInnerHTML) {
|
||||||
|
// 检查是否包含表格
|
||||||
|
if (nodeEle.innerHTML.includes('<table')) {
|
||||||
|
this.editTableNode(nodeEle)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 其他dangerouslySetInnerHTML节点暂时不支持编辑
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
this.editTopic(nodeEle)
|
this.editTopic(nodeEle)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,319 +1,608 @@
|
||||||
import { LEFT } from '../const'
|
import { LEFT } from '../const'
|
||||||
import type { Topic, Wrapper, Parent, Children, Expander } from '../types/dom'
|
import type { Topic, Wrapper, Parent, Children, Expander } from '../types/dom'
|
||||||
import type { MindElixirInstance, NodeObj } from '../types/index'
|
import type { MindElixirInstance, NodeObj } from '../types/index'
|
||||||
import { encodeHTML, getOffsetLT } from '../utils/index'
|
import { encodeHTML, getOffsetLT } from '../utils/index'
|
||||||
import { layoutChildren } from './layout'
|
import { layoutChildren } from './layout'
|
||||||
// 移除imageProcessor引用,使用MindElixir原生image属性
|
import { marked } from 'marked'
|
||||||
|
// 移除imageProcessor引用,使用MindElixir原生image属性
|
||||||
|
|
||||||
// DOM manipulation
|
|
||||||
const $d = document
|
// DOM manipulation
|
||||||
export const findEle = function (this: MindElixirInstance, id: string, el?: HTMLElement) {
|
const $d = document
|
||||||
const scope = this?.el ? this.el : el ? el : document
|
export const findEle = function (this: MindElixirInstance, id: string, el?: HTMLElement) {
|
||||||
const ele = scope.querySelector<Topic>(`[data-nodeid="me${id}"]`)
|
const scope = this?.el ? this.el : el ? el : document
|
||||||
if (!ele) throw new Error(`FindEle: Node ${id} not found, maybe it's collapsed.`)
|
const ele = scope.querySelector<Topic>(`[data-nodeid="me${id}"]`)
|
||||||
return ele
|
if (!ele) throw new Error(`FindEle: Node ${id} not found, maybe it's collapsed.`)
|
||||||
}
|
return ele
|
||||||
|
}
|
||||||
export const shapeTpc = function (this: MindElixirInstance, tpc: Topic, nodeObj: NodeObj) {
|
|
||||||
tpc.innerHTML = ''
|
export const shapeTpc = function (this: MindElixirInstance, tpc: Topic, nodeObj: NodeObj) {
|
||||||
|
tpc.innerHTML = ''
|
||||||
if (nodeObj.style) {
|
|
||||||
const style = nodeObj.style
|
if (nodeObj.style) {
|
||||||
type KeyOfStyle = keyof typeof style
|
const style = nodeObj.style
|
||||||
for (const key in style) {
|
type KeyOfStyle = keyof typeof style
|
||||||
tpc.style[key as KeyOfStyle] = style[key as KeyOfStyle]!
|
for (const key in style) {
|
||||||
}
|
tpc.style[key as KeyOfStyle] = style[key as KeyOfStyle]!
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (nodeObj.dangerouslySetInnerHTML) {
|
|
||||||
// 清理HTML内容,移除重复的格式
|
if (nodeObj.dangerouslySetInnerHTML) {
|
||||||
let cleanedHTML = nodeObj.dangerouslySetInnerHTML
|
// 清理HTML内容,移除重复的格式
|
||||||
|
let cleanedHTML = nodeObj.dangerouslySetInnerHTML
|
||||||
// 检查是否包含表格,如果包含表格则不进行列表处理
|
|
||||||
const hasTable = cleanedHTML.includes('<table') || cleanedHTML.includes('<td>') || cleanedHTML.includes('<th>')
|
// 检查是否包含表格,如果包含表格则不进行列表处理
|
||||||
|
const hasTable = cleanedHTML.includes('<table') || cleanedHTML.includes('<td>') || cleanedHTML.includes('<th>')
|
||||||
if (!hasTable) {
|
|
||||||
// 移除• 【这种重复格式
|
if (!hasTable) {
|
||||||
cleanedHTML = cleanedHTML.replace(/•\s*【/g, '【')
|
// 移除• 【这种重复格式
|
||||||
cleanedHTML = cleanedHTML.replace(/•\s*\[/g, '[')
|
cleanedHTML = cleanedHTML.replace(/•\s*【/g, '【')
|
||||||
|
cleanedHTML = cleanedHTML.replace(/•\s*\[/g, '[')
|
||||||
// 移除其他可能的重复格式
|
|
||||||
cleanedHTML = cleanedHTML.replace(/•\s*(/g, '(')
|
// 移除其他可能的重复格式
|
||||||
cleanedHTML = cleanedHTML.replace(/•\s*\(/g, '(')
|
cleanedHTML = cleanedHTML.replace(/•\s*(/g, '(')
|
||||||
|
cleanedHTML = cleanedHTML.replace(/•\s*\(/g, '(')
|
||||||
// 处理换行符和列表格式
|
|
||||||
// 将换行符转换为<br>标签,但保持列表结构
|
// 处理换行符和列表格式
|
||||||
cleanedHTML = cleanedHTML
|
// 将换行符转换为<br>标签,但保持列表结构
|
||||||
// 处理列表项:将每个•开头的行转换为<li>
|
cleanedHTML = cleanedHTML
|
||||||
.replace(/^(\s*)•\s*(.+)$/gm, '<li>$2</li>')
|
// 处理列表项:将每个•开头的行转换为<li>
|
||||||
// 将连续的<li>标签包装在<ul>中
|
.replace(/^(\s*)•\s*(.+)$/gm, '<li>$2</li>')
|
||||||
.replace(/(<li>.*<\/li>)/gs, (match) => {
|
// 将连续的<li>标签包装在<ul>中
|
||||||
// 如果已经被<ul>包装,不重复包装
|
.replace(/(<li>.*<\/li>)/gs, (match) => {
|
||||||
if (cleanedHTML.includes('<ul>')) return match
|
// 如果已经被<ul>包装,不重复包装
|
||||||
return `<ul>${match}</ul>`
|
if (cleanedHTML.includes('<ul>')) return match
|
||||||
})
|
return `<ul>${match}</ul>`
|
||||||
// 将剩余的换行符转换为<br>
|
})
|
||||||
.replace(/\n/g, '<br>')
|
// 将剩余的换行符转换为<br>
|
||||||
} else {
|
.replace(/\n/g, '<br>')
|
||||||
// 对于表格内容,只移除重复格式,不进行列表处理
|
} else {
|
||||||
cleanedHTML = cleanedHTML.replace(/•\s*【/g, '【')
|
// 对于表格内容,只移除重复格式,不进行列表处理
|
||||||
cleanedHTML = cleanedHTML.replace(/•\s*\[/g, '[')
|
cleanedHTML = cleanedHTML.replace(/•\s*【/g, '【')
|
||||||
cleanedHTML = cleanedHTML.replace(/•\s*(/g, '(')
|
cleanedHTML = cleanedHTML.replace(/•\s*\[/g, '[')
|
||||||
cleanedHTML = cleanedHTML.replace(/•\s*\(/g, '(')
|
cleanedHTML = cleanedHTML.replace(/•\s*(/g, '(')
|
||||||
}
|
cleanedHTML = cleanedHTML.replace(/•\s*\(/g, '(')
|
||||||
|
}
|
||||||
tpc.innerHTML = cleanedHTML
|
|
||||||
|
tpc.innerHTML = cleanedHTML
|
||||||
// 检查是否包含图片,决定文本对齐方式
|
|
||||||
const hasImages = cleanedHTML.includes('<img')
|
// 检查是否包含图片,决定文本对齐方式
|
||||||
const hasNodeImage = !!nodeObj.image
|
const hasImages = cleanedHTML.includes('<img')
|
||||||
const hasAnyImage = hasImages || hasNodeImage
|
const hasNodeImage = !!nodeObj.image
|
||||||
|
const hasAnyImage = hasImages || hasNodeImage
|
||||||
// 统一使用左对齐样式,简化逻辑
|
|
||||||
tpc.classList.add('no-image')
|
// 统一使用左对齐样式,简化逻辑
|
||||||
tpc.classList.remove('has-image')
|
tpc.classList.add('no-image')
|
||||||
|
tpc.classList.remove('has-image')
|
||||||
return
|
|
||||||
}
|
return
|
||||||
|
}
|
||||||
if (nodeObj.image) {
|
|
||||||
const img = nodeObj.image
|
if (nodeObj.image) {
|
||||||
if (img.url && img.width && img.height) {
|
const img = nodeObj.image
|
||||||
const imgEl = $d.createElement('img')
|
if (img.url && img.width && img.height) {
|
||||||
// Use imageProxy function if provided, otherwise use original URL
|
const imgEl = $d.createElement('img')
|
||||||
imgEl.src = this.imageProxy ? this.imageProxy(img.url) : img.url
|
// Use imageProxy function if provided, otherwise use original URL
|
||||||
imgEl.style.width = img.width + 'px'
|
imgEl.src = this.imageProxy ? this.imageProxy(img.url) : img.url
|
||||||
imgEl.style.height = img.height + 'px'
|
imgEl.style.width = img.width + 'px'
|
||||||
if (img.fit) imgEl.style.objectFit = img.fit
|
imgEl.style.height = img.height + 'px'
|
||||||
tpc.appendChild(imgEl)
|
if (img.fit) imgEl.style.objectFit = img.fit
|
||||||
tpc.image = imgEl
|
tpc.appendChild(imgEl)
|
||||||
} else {
|
tpc.image = imgEl
|
||||||
console.warn('Image url/width/height are required')
|
} else {
|
||||||
}
|
console.warn('Image url/width/height are required')
|
||||||
} else if (tpc.image) {
|
}
|
||||||
tpc.image = undefined
|
} else if (tpc.image) {
|
||||||
}
|
tpc.image = undefined
|
||||||
|
}
|
||||||
{
|
|
||||||
const textEl = $d.createElement('span')
|
{
|
||||||
textEl.className = 'text'
|
const textEl = $d.createElement('span')
|
||||||
|
textEl.className = 'text'
|
||||||
// Check if markdown parser is provided and topic contains markdown syntax
|
|
||||||
let content = ''
|
// Check if markdown parser is provided and topic contains markdown syntax
|
||||||
if (this.markdown) {
|
let content = ''
|
||||||
content = this.markdown(nodeObj.topic, nodeObj)
|
if (this.markdown) {
|
||||||
} else {
|
content = this.markdown(nodeObj.topic, nodeObj)
|
||||||
// 直接设置文本内容,图片通过MindElixir原生image属性处理
|
} else {
|
||||||
content = nodeObj.topic || ''
|
// 直接设置文本内容,图片通过MindElixir原生image属性处理
|
||||||
}
|
content = nodeObj.topic || ''
|
||||||
|
}
|
||||||
// 清理文本内容,移除重复的格式
|
|
||||||
content = content.replace(/•\s*【/g, '【')
|
// 清理文本内容,移除重复的格式
|
||||||
content = content.replace(/•\s*\[/g, '[')
|
content = content.replace(/•\s*【/g, '【')
|
||||||
content = content.replace(/•\s*(/g, '(')
|
content = content.replace(/•\s*\[/g, '[')
|
||||||
content = content.replace(/•\s*\(/g, '(')
|
content = content.replace(/•\s*(/g, '(')
|
||||||
|
content = content.replace(/•\s*\(/g, '(')
|
||||||
textEl.innerHTML = content
|
|
||||||
tpc.appendChild(textEl)
|
textEl.innerHTML = content
|
||||||
tpc.text = textEl
|
tpc.appendChild(textEl)
|
||||||
|
tpc.text = textEl
|
||||||
// 检查是否有图片,决定文本对齐方式
|
|
||||||
const hasNodeImage = !!nodeObj.image
|
// 检查是否有图片,决定文本对齐方式
|
||||||
const hasImageInText = content.includes('<img')
|
const hasNodeImage = !!nodeObj.image
|
||||||
const hasAnyImage = hasNodeImage || hasImageInText
|
const hasImageInText = content.includes('<img')
|
||||||
|
const hasAnyImage = hasNodeImage || hasImageInText
|
||||||
// 统一使用左对齐样式,简化逻辑
|
|
||||||
tpc.classList.add('no-image')
|
// 统一使用左对齐样式,简化逻辑
|
||||||
tpc.classList.remove('has-image')
|
tpc.classList.add('no-image')
|
||||||
}
|
tpc.classList.remove('has-image')
|
||||||
|
}
|
||||||
if (nodeObj.hyperLink) {
|
|
||||||
const linkEl = $d.createElement('a')
|
if (nodeObj.hyperLink) {
|
||||||
linkEl.className = 'hyper-link'
|
const linkEl = $d.createElement('a')
|
||||||
linkEl.target = '_blank'
|
linkEl.className = 'hyper-link'
|
||||||
linkEl.innerText = '🔗'
|
linkEl.target = '_blank'
|
||||||
linkEl.href = nodeObj.hyperLink
|
linkEl.innerText = '🔗'
|
||||||
tpc.appendChild(linkEl)
|
linkEl.href = nodeObj.hyperLink
|
||||||
tpc.link = linkEl
|
tpc.appendChild(linkEl)
|
||||||
} else if (tpc.link) {
|
tpc.link = linkEl
|
||||||
tpc.link = undefined
|
} else if (tpc.link) {
|
||||||
}
|
tpc.link = undefined
|
||||||
|
}
|
||||||
if (nodeObj.icons && nodeObj.icons.length) {
|
|
||||||
const iconsEl = $d.createElement('span')
|
if (nodeObj.icons && nodeObj.icons.length) {
|
||||||
iconsEl.className = 'icons'
|
const iconsEl = $d.createElement('span')
|
||||||
iconsEl.innerHTML = nodeObj.icons.map(icon => `<span>${encodeHTML(icon)}</span>`).join('')
|
iconsEl.className = 'icons'
|
||||||
tpc.appendChild(iconsEl)
|
iconsEl.innerHTML = nodeObj.icons.map(icon => `<span>${encodeHTML(icon)}</span>`).join('')
|
||||||
tpc.icons = iconsEl
|
tpc.appendChild(iconsEl)
|
||||||
} else if (tpc.icons) {
|
tpc.icons = iconsEl
|
||||||
tpc.icons = undefined
|
} else if (tpc.icons) {
|
||||||
}
|
tpc.icons = undefined
|
||||||
|
}
|
||||||
if (nodeObj.tags && nodeObj.tags.length) {
|
|
||||||
const tagsEl = $d.createElement('div')
|
if (nodeObj.tags && nodeObj.tags.length) {
|
||||||
tagsEl.className = 'tags'
|
const tagsEl = $d.createElement('div')
|
||||||
|
tagsEl.className = 'tags'
|
||||||
nodeObj.tags.forEach(tag => {
|
|
||||||
const span = $d.createElement('span')
|
nodeObj.tags.forEach(tag => {
|
||||||
|
const span = $d.createElement('span')
|
||||||
if (typeof tag === 'string') {
|
|
||||||
span.textContent = tag
|
if (typeof tag === 'string') {
|
||||||
} else {
|
span.textContent = tag
|
||||||
span.textContent = tag.text
|
} else {
|
||||||
if (tag.className) {
|
span.textContent = tag.text
|
||||||
span.className = tag.className
|
if (tag.className) {
|
||||||
}
|
span.className = tag.className
|
||||||
if (tag.style) {
|
}
|
||||||
Object.assign(span.style, tag.style)
|
if (tag.style) {
|
||||||
}
|
Object.assign(span.style, tag.style)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
tagsEl.appendChild(span)
|
|
||||||
})
|
tagsEl.appendChild(span)
|
||||||
|
})
|
||||||
tpc.appendChild(tagsEl)
|
|
||||||
tpc.tags = tagsEl
|
tpc.appendChild(tagsEl)
|
||||||
} else if (tpc.tags) {
|
tpc.tags = tagsEl
|
||||||
tpc.tags = undefined
|
} else if (tpc.tags) {
|
||||||
}
|
tpc.tags = undefined
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// everything start from `Wrapper`
|
|
||||||
export const createWrapper = function (this: MindElixirInstance, nodeObj: NodeObj, omitChildren?: boolean) {
|
// everything start from `Wrapper`
|
||||||
const grp = $d.createElement('me-wrapper') as Wrapper
|
export const createWrapper = function (this: MindElixirInstance, nodeObj: NodeObj, omitChildren?: boolean) {
|
||||||
const { p, tpc } = this.createParent(nodeObj)
|
const grp = $d.createElement('me-wrapper') as Wrapper
|
||||||
grp.appendChild(p)
|
const { p, tpc } = this.createParent(nodeObj)
|
||||||
if (!omitChildren && nodeObj.children && nodeObj.children.length > 0) {
|
grp.appendChild(p)
|
||||||
const expander = createExpander(nodeObj.expanded)
|
if (!omitChildren && nodeObj.children && nodeObj.children.length > 0) {
|
||||||
p.appendChild(expander)
|
const expander = createExpander(nodeObj.expanded)
|
||||||
// tpc.expander = expander
|
p.appendChild(expander)
|
||||||
if (nodeObj.expanded !== false) {
|
// tpc.expander = expander
|
||||||
const children = layoutChildren(this, nodeObj.children)
|
if (nodeObj.expanded !== false) {
|
||||||
grp.appendChild(children)
|
const children = layoutChildren(this, nodeObj.children)
|
||||||
}
|
grp.appendChild(children)
|
||||||
}
|
}
|
||||||
return { grp, top: p, tpc }
|
}
|
||||||
}
|
return { grp, top: p, tpc }
|
||||||
|
}
|
||||||
export const createParent = function (this: MindElixirInstance, nodeObj: NodeObj) {
|
|
||||||
const p = $d.createElement('me-parent') as Parent
|
export const createParent = function (this: MindElixirInstance, nodeObj: NodeObj) {
|
||||||
const tpc = this.createTopic(nodeObj)
|
const p = $d.createElement('me-parent') as Parent
|
||||||
shapeTpc.call(this, tpc, nodeObj)
|
const tpc = this.createTopic(nodeObj)
|
||||||
p.appendChild(tpc)
|
shapeTpc.call(this, tpc, nodeObj)
|
||||||
|
p.appendChild(tpc)
|
||||||
return { p, tpc }
|
|
||||||
}
|
return { p, tpc }
|
||||||
|
}
|
||||||
|
|
||||||
export const createChildren = function (this: MindElixirInstance, wrappers: Wrapper[]) {
|
|
||||||
const children = $d.createElement('me-children') as Children
|
export const createChildren = function (this: MindElixirInstance, wrappers: Wrapper[]) {
|
||||||
children.append(...wrappers)
|
const children = $d.createElement('me-children') as Children
|
||||||
return children
|
children.append(...wrappers)
|
||||||
}
|
return children
|
||||||
|
}
|
||||||
export const createTopic = function (this: MindElixirInstance, nodeObj: NodeObj) {
|
|
||||||
const topic = $d.createElement('me-tpc') as Topic
|
export const createTopic = function (this: MindElixirInstance, nodeObj: NodeObj) {
|
||||||
topic.nodeObj = nodeObj
|
const topic = $d.createElement('me-tpc') as Topic
|
||||||
topic.dataset.nodeid = 'me' + nodeObj.id
|
topic.nodeObj = nodeObj
|
||||||
topic.draggable = this.draggable
|
topic.dataset.nodeid = 'me' + nodeObj.id
|
||||||
return topic
|
topic.draggable = this.draggable
|
||||||
}
|
return topic
|
||||||
|
}
|
||||||
export function selectText(div: HTMLElement) {
|
|
||||||
const range = $d.createRange()
|
export function selectText(div: HTMLElement) {
|
||||||
range.selectNodeContents(div)
|
const range = $d.createRange()
|
||||||
const getSelection = window.getSelection()
|
range.selectNodeContents(div)
|
||||||
if (getSelection) {
|
const getSelection = window.getSelection()
|
||||||
getSelection.removeAllRanges()
|
if (getSelection) {
|
||||||
getSelection.addRange(range)
|
getSelection.removeAllRanges()
|
||||||
}
|
getSelection.addRange(range)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
export const editTopic = function (this: MindElixirInstance, el: Topic) {
|
|
||||||
console.time('editTopic')
|
export const editTopic = function (this: MindElixirInstance, el: Topic) {
|
||||||
if (!el) return
|
console.time('editTopic')
|
||||||
const div = $d.createElement('div')
|
if (!el) return
|
||||||
const node = el.nodeObj
|
const div = $d.createElement('div')
|
||||||
|
const node = el.nodeObj
|
||||||
// Get the original content from topic
|
|
||||||
const originalContent = node.topic
|
// Get the original content from topic
|
||||||
|
const originalContent = node.topic
|
||||||
el.appendChild(div)
|
|
||||||
div.id = 'input-box'
|
el.appendChild(div)
|
||||||
div.textContent = originalContent
|
div.id = 'input-box'
|
||||||
div.contentEditable = 'plaintext-only'
|
div.textContent = originalContent
|
||||||
div.spellcheck = false
|
div.contentEditable = 'plaintext-only'
|
||||||
const style = getComputedStyle(el)
|
div.spellcheck = false
|
||||||
div.style.cssText = `min-width:${el.offsetWidth - 8}px;
|
const style = getComputedStyle(el)
|
||||||
color:${style.color};
|
div.style.cssText = `min-width:${el.offsetWidth - 8}px;
|
||||||
padding:${style.padding};
|
color:${style.color};
|
||||||
margin:${style.margin};
|
padding:${style.padding};
|
||||||
font:${style.font};
|
margin:${style.margin};
|
||||||
background-color:${style.backgroundColor !== 'rgba(0, 0, 0, 0)' && style.backgroundColor};
|
font:${style.font};
|
||||||
border-radius:${style.borderRadius};`
|
background-color:${style.backgroundColor !== 'rgba(0, 0, 0, 0)' && style.backgroundColor};
|
||||||
if (this.direction === LEFT) div.style.right = '0'
|
border-radius:${style.borderRadius};`
|
||||||
|
if (this.direction === LEFT) div.style.right = '0'
|
||||||
selectText(div)
|
|
||||||
|
selectText(div)
|
||||||
this.bus.fire('operation', {
|
|
||||||
name: 'beginEdit',
|
this.bus.fire('operation', {
|
||||||
obj: el.nodeObj,
|
name: 'beginEdit',
|
||||||
})
|
obj: el.nodeObj,
|
||||||
|
})
|
||||||
div.addEventListener('keydown', e => {
|
|
||||||
e.stopPropagation()
|
div.addEventListener('keydown', e => {
|
||||||
const key = e.key
|
e.stopPropagation()
|
||||||
|
const key = e.key
|
||||||
if (key === 'Enter' || key === 'Tab') {
|
|
||||||
// keep wrap for shift enter
|
if (key === 'Enter' || key === 'Tab') {
|
||||||
if (e.shiftKey) return
|
// keep wrap for shift enter
|
||||||
|
if (e.shiftKey) return
|
||||||
e.preventDefault()
|
|
||||||
div.blur()
|
e.preventDefault()
|
||||||
this.container.focus()
|
div.blur()
|
||||||
}
|
this.container.focus()
|
||||||
})
|
}
|
||||||
|
})
|
||||||
div.addEventListener('blur', () => {
|
|
||||||
if (!div) return
|
div.addEventListener('blur', () => {
|
||||||
const inputContent = div.textContent?.trim() || ''
|
if (!div) return
|
||||||
|
const inputContent = div.textContent?.trim() || ''
|
||||||
if (inputContent === '') {
|
|
||||||
node.topic = originalContent
|
if (inputContent === '') {
|
||||||
} else {
|
node.topic = originalContent
|
||||||
// Update topic content
|
} else {
|
||||||
node.topic = inputContent
|
// Update topic content
|
||||||
|
node.topic = inputContent
|
||||||
if (this.markdown) {
|
|
||||||
el.text.innerHTML = this.markdown(node.topic, node)
|
if (this.markdown) {
|
||||||
} else {
|
el.text.innerHTML = this.markdown(node.topic, node)
|
||||||
// Plain text content
|
} else {
|
||||||
el.text.textContent = inputContent
|
// Plain text content
|
||||||
}
|
el.text.textContent = inputContent
|
||||||
}
|
}
|
||||||
|
}
|
||||||
div.remove()
|
|
||||||
|
div.remove()
|
||||||
// 重新应用样式和清理逻辑
|
|
||||||
shapeTpc.call(this, el, node)
|
// 重新应用样式和清理逻辑
|
||||||
|
shapeTpc.call(this, el, node)
|
||||||
if (inputContent === originalContent) return
|
|
||||||
|
if (inputContent === originalContent) return
|
||||||
this.linkDiv()
|
|
||||||
this.bus.fire('operation', {
|
this.linkDiv()
|
||||||
name: 'finishEdit',
|
this.bus.fire('operation', {
|
||||||
obj: node,
|
name: 'finishEdit',
|
||||||
origin: originalContent,
|
obj: node,
|
||||||
})
|
origin: originalContent,
|
||||||
})
|
})
|
||||||
console.timeEnd('editTopic')
|
})
|
||||||
}
|
console.timeEnd('editTopic')
|
||||||
|
}
|
||||||
export const createExpander = function (expanded: boolean | undefined): Expander {
|
|
||||||
const expander = $d.createElement('me-epd') as Expander
|
export const createExpander = function (expanded: boolean | undefined): Expander {
|
||||||
// if expanded is undefined, treat as expanded
|
const expander = $d.createElement('me-epd') as Expander
|
||||||
expander.expanded = expanded !== false
|
// if expanded is undefined, treat as expanded
|
||||||
expander.className = expanded !== false ? 'minus' : ''
|
expander.expanded = expanded !== false
|
||||||
return expander
|
expander.className = expanded !== false ? 'minus' : ''
|
||||||
}
|
return expander
|
||||||
|
}
|
||||||
|
|
||||||
|
// 表格编辑功能
|
||||||
|
export const editTableNode = function (this: MindElixirInstance, el: Topic) {
|
||||||
|
console.time('editTableNode')
|
||||||
|
if (!el) return
|
||||||
|
|
||||||
|
const node = el.nodeObj
|
||||||
|
const originalHTML = node.dangerouslySetInnerHTML || ''
|
||||||
|
|
||||||
|
// 创建遮罩层
|
||||||
|
const overlay = $d.createElement('div')
|
||||||
|
overlay.id = 'table-edit-overlay'
|
||||||
|
overlay.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
z-index: 9999;
|
||||||
|
`
|
||||||
|
|
||||||
|
// 创建编辑容器 - 使用fixed定位,居中显示
|
||||||
|
const editContainer = $d.createElement('div')
|
||||||
|
editContainer.id = 'table-edit-container'
|
||||||
|
editContainer.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 600px;
|
||||||
|
min-height: 400px;
|
||||||
|
max-height: 80vh;
|
||||||
|
background: white;
|
||||||
|
border: 2px solid #007bff;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 15px;
|
||||||
|
z-index: 10000;
|
||||||
|
box-shadow: 0 8px 24px rgba(0,0,0,0.3);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
`
|
||||||
|
|
||||||
|
// 创建标题
|
||||||
|
const title = $d.createElement('div')
|
||||||
|
title.textContent = '编辑表格(Markdown格式)'
|
||||||
|
title.style.cssText = `
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
`
|
||||||
|
|
||||||
|
// 创建文本区域用于编辑表格markdown
|
||||||
|
const textarea = $d.createElement('textarea')
|
||||||
|
textarea.value = this.convertTableHTMLToMarkdown(originalHTML)
|
||||||
|
textarea.style.cssText = `
|
||||||
|
flex: 1;
|
||||||
|
min-height: 300px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 12px;
|
||||||
|
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
resize: vertical;
|
||||||
|
outline: none;
|
||||||
|
background: #fafafa;
|
||||||
|
color: #333;
|
||||||
|
caret-color: #007bff;
|
||||||
|
`
|
||||||
|
textarea.setAttribute('spellcheck', 'false')
|
||||||
|
textarea.setAttribute('autocomplete', 'off')
|
||||||
|
textarea.setAttribute('autocorrect', 'off')
|
||||||
|
textarea.setAttribute('autocapitalize', 'off')
|
||||||
|
|
||||||
|
// 创建按钮容器
|
||||||
|
const buttonContainer = $d.createElement('div')
|
||||||
|
buttonContainer.style.cssText = `
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 12px;
|
||||||
|
padding-top: 12px;
|
||||||
|
border-top: 1px solid #e0e0e0;
|
||||||
|
`
|
||||||
|
|
||||||
|
// 创建提示文字
|
||||||
|
const hint = $d.createElement('span')
|
||||||
|
hint.textContent = '提示: Ctrl+Enter保存, Esc取消'
|
||||||
|
hint.style.cssText = `
|
||||||
|
font-size: 11px;
|
||||||
|
color: #999;
|
||||||
|
`
|
||||||
|
|
||||||
|
// 创建按钮组
|
||||||
|
const buttonGroup = $d.createElement('div')
|
||||||
|
buttonGroup.style.cssText = `
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
`
|
||||||
|
|
||||||
|
// 创建取消按钮
|
||||||
|
const cancelButton = $d.createElement('button')
|
||||||
|
cancelButton.textContent = '取消'
|
||||||
|
cancelButton.style.cssText = `
|
||||||
|
padding: 8px 20px;
|
||||||
|
background: #6c757d;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
transition: background 0.2s;
|
||||||
|
`
|
||||||
|
cancelButton.addEventListener('mouseenter', () => {
|
||||||
|
cancelButton.style.background = '#5a6268'
|
||||||
|
})
|
||||||
|
cancelButton.addEventListener('mouseleave', () => {
|
||||||
|
cancelButton.style.background = '#6c757d'
|
||||||
|
})
|
||||||
|
|
||||||
|
// 创建保存按钮
|
||||||
|
const saveButton = $d.createElement('button')
|
||||||
|
saveButton.textContent = '保存'
|
||||||
|
saveButton.style.cssText = `
|
||||||
|
padding: 8px 20px;
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: background 0.2s;
|
||||||
|
`
|
||||||
|
saveButton.addEventListener('mouseenter', () => {
|
||||||
|
saveButton.style.background = '#0056b3'
|
||||||
|
})
|
||||||
|
saveButton.addEventListener('mouseleave', () => {
|
||||||
|
saveButton.style.background = '#007bff'
|
||||||
|
})
|
||||||
|
|
||||||
|
buttonGroup.appendChild(cancelButton)
|
||||||
|
buttonGroup.appendChild(saveButton)
|
||||||
|
buttonContainer.appendChild(hint)
|
||||||
|
buttonContainer.appendChild(buttonGroup)
|
||||||
|
|
||||||
|
editContainer.appendChild(title)
|
||||||
|
editContainer.appendChild(textarea)
|
||||||
|
editContainer.appendChild(buttonContainer)
|
||||||
|
|
||||||
|
// 添加到body(因为使用了fixed定位)
|
||||||
|
$d.body.appendChild(overlay)
|
||||||
|
$d.body.appendChild(editContainer)
|
||||||
|
|
||||||
|
// 点击遮罩层关闭编辑器
|
||||||
|
overlay.addEventListener('click', () => {
|
||||||
|
overlay.remove()
|
||||||
|
editContainer.remove()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 延迟聚焦,确保DOM已渲染
|
||||||
|
setTimeout(() => {
|
||||||
|
textarea.focus()
|
||||||
|
textarea.setSelectionRange(0, 0) // 光标移到开始
|
||||||
|
console.log('📊 编辑器已聚焦,光标应该可见')
|
||||||
|
}, 50)
|
||||||
|
|
||||||
|
// 保存功能
|
||||||
|
const saveTable = () => {
|
||||||
|
const newMarkdown = textarea.value.trim()
|
||||||
|
|
||||||
|
console.log('📊 保存表格,原始markdown:', newMarkdown)
|
||||||
|
|
||||||
|
if (newMarkdown === this.convertTableHTMLToMarkdown(originalHTML)) {
|
||||||
|
// 没有变化,直接关闭
|
||||||
|
console.log('📊 表格内容未变化,直接关闭')
|
||||||
|
overlay.remove()
|
||||||
|
editContainer.remove()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 配置marked
|
||||||
|
marked.setOptions({
|
||||||
|
breaks: false,
|
||||||
|
gfm: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 转换markdown为HTML (marked.parse返回Promise或string)
|
||||||
|
let newHTML = marked.parse(newMarkdown) as string
|
||||||
|
console.log('📊 转换后的HTML:', newHTML)
|
||||||
|
|
||||||
|
// 为表格添加样式类
|
||||||
|
const styledHTML = newHTML.replace(/<table>/g, '<table class="markdown-table">')
|
||||||
|
|
||||||
|
console.log('📊 更新节点,新HTML长度:', styledHTML.length)
|
||||||
|
|
||||||
|
// 移除编辑容器和遮罩层
|
||||||
|
overlay.remove()
|
||||||
|
editContainer.remove()
|
||||||
|
|
||||||
|
// 更新节点数据
|
||||||
|
node.dangerouslySetInnerHTML = styledHTML
|
||||||
|
node.topic = newMarkdown // 同时保存markdown原文
|
||||||
|
|
||||||
|
console.log('📊 直接更新DOM内容')
|
||||||
|
// 直接更新DOM,不调用shapeTpc(因为shapeTpc会清空innerHTML)
|
||||||
|
el.innerHTML = styledHTML
|
||||||
|
|
||||||
|
// 添加样式类
|
||||||
|
el.classList.add('no-image')
|
||||||
|
el.classList.remove('has-image')
|
||||||
|
|
||||||
|
console.log('📊 触发操作事件')
|
||||||
|
// 触发操作事件
|
||||||
|
this.bus.fire('operation', {
|
||||||
|
name: 'finishEditTable',
|
||||||
|
obj: node,
|
||||||
|
origin: originalHTML,
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('📊 重新布局')
|
||||||
|
this.linkDiv()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 取消功能
|
||||||
|
const cancelEdit = () => {
|
||||||
|
overlay.remove()
|
||||||
|
editContainer.remove()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绑定事件
|
||||||
|
saveButton.addEventListener('click', saveTable)
|
||||||
|
cancelButton.addEventListener('click', cancelEdit)
|
||||||
|
|
||||||
|
// 键盘事件
|
||||||
|
textarea.addEventListener('keydown', (e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
e.preventDefault()
|
||||||
|
cancelEdit()
|
||||||
|
} else if (e.key === 'Enter' && e.ctrlKey) {
|
||||||
|
e.preventDefault()
|
||||||
|
saveTable()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
console.timeEnd('editTableNode')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将表格HTML转换为markdown
|
||||||
|
export const convertTableHTMLToMarkdown = function (this: MindElixirInstance, html: string): string {
|
||||||
|
// 创建临时DOM元素
|
||||||
|
const tempDiv = $d.createElement('div')
|
||||||
|
tempDiv.innerHTML = html
|
||||||
|
|
||||||
|
const table = tempDiv.querySelector('table')
|
||||||
|
if (!table) return html
|
||||||
|
|
||||||
|
let markdown = ''
|
||||||
|
|
||||||
|
// 处理表头
|
||||||
|
const thead = table.querySelector('thead')
|
||||||
|
if (thead) {
|
||||||
|
const headerRow = thead.querySelector('tr')
|
||||||
|
if (headerRow) {
|
||||||
|
const headers = Array.from(headerRow.querySelectorAll('th')).map(th => th.textContent?.trim() || '')
|
||||||
|
markdown += '| ' + headers.join(' | ') + ' |\n'
|
||||||
|
markdown += '| ' + headers.map(() => '---').join(' | ') + ' |\n'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理表体
|
||||||
|
const tbody = table.querySelector('tbody')
|
||||||
|
if (tbody) {
|
||||||
|
const rows = tbody.querySelectorAll('tr')
|
||||||
|
rows.forEach(row => {
|
||||||
|
const cells = Array.from(row.querySelectorAll('td')).map(td => td.textContent?.trim() || '')
|
||||||
|
markdown += '| ' + cells.join(' | ') + ' |\n'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return markdown.trim()
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,11 @@ type NodeOperation =
|
||||||
obj: NodeObj
|
obj: NodeObj
|
||||||
origin: string
|
origin: string
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
name: 'finishEditTable'
|
||||||
|
obj: NodeObj
|
||||||
|
origin: string
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
name: 'moveNodeAfter' | 'moveNodeBefore' | 'moveNodeIn'
|
name: 'moveNodeAfter' | 'moveNodeBefore' | 'moveNodeIn'
|
||||||
objs: NodeObj[]
|
objs: NodeObj[]
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
export declare function renderMarkdownToHTML(markdown: string): string;
|
||||||
|
export declare function hasMarkdownSyntax(content: string): boolean;
|
||||||
|
export declare function preprocessMarkdown(markdown: string): string;
|
||||||
|
export declare function postprocessHTML(html: string): string;
|
||||||
|
|
@ -14,9 +14,26 @@ import 'prismjs/components/prism-python';
|
||||||
import 'prismjs/components/prism-sql';
|
import 'prismjs/components/prism-sql';
|
||||||
import 'katex/dist/katex.min.css';
|
import 'katex/dist/katex.min.css';
|
||||||
|
|
||||||
// 自定义渲染器(移除图片处理,使用MindElixir原生image属性)
|
// 自定义渲染器(支持图片渲染)
|
||||||
const renderer = new marked.Renderer();
|
const renderer = new marked.Renderer();
|
||||||
|
|
||||||
|
// 自定义图片渲染器
|
||||||
|
renderer.image = function(href, title, text) {
|
||||||
|
// 处理图片URL,确保能正确显示
|
||||||
|
let processedUrl = href;
|
||||||
|
if (href.includes('cdn-mineru.openxlab.org.cn')) {
|
||||||
|
// 将外部CDN URL转换为代理URL
|
||||||
|
const urlPath = href.replace('https://cdn-mineru.openxlab.org.cn', '');
|
||||||
|
processedUrl = `/proxy-image${urlPath}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成图片HTML
|
||||||
|
const altText = text || '图片';
|
||||||
|
const titleAttr = title ? ` title="${title}"` : '';
|
||||||
|
|
||||||
|
return `<img src="${processedUrl}" alt="${altText}"${titleAttr} class="markdown-image" style="max-width: 200px; max-height: 150px; object-fit: contain; display: block; margin: 4px auto; border-radius: 4px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);" />`;
|
||||||
|
};
|
||||||
|
|
||||||
// 配置marked选项
|
// 配置marked选项
|
||||||
marked.setOptions({
|
marked.setOptions({
|
||||||
breaks: false, // 禁用breaks,避免在表格中产生<br>标签导致HTML不匹配
|
breaks: false, // 禁用breaks,避免在表格中产生<br>标签导致HTML不匹配
|
||||||
|
|
@ -409,6 +426,26 @@ const addMarkdownStyles = (container) => {
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.markdown-image {
|
||||||
|
max-width: 200px !important;
|
||||||
|
max-height: 150px !important;
|
||||||
|
width: auto !important;
|
||||||
|
height: auto !important;
|
||||||
|
object-fit: contain !important;
|
||||||
|
display: block !important;
|
||||||
|
margin: 4px auto !important;
|
||||||
|
border-radius: 4px !important;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1) !important;
|
||||||
|
background-color: #f5f5f5 !important;
|
||||||
|
cursor: pointer !important;
|
||||||
|
transition: transform 0.2s ease !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-image:hover {
|
||||||
|
transform: scale(1.02) !important;
|
||||||
|
box-shadow: 0 4px 8px rgba(0,0,0,0.15) !important;
|
||||||
|
}
|
||||||
|
|
||||||
.markdown-error {
|
.markdown-error {
|
||||||
color: #dc3545;
|
color: #dc3545;
|
||||||
background: #f8d7da;
|
background: #f8d7da;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue