2025-10-11 06:07:16 +00:00
|
|
|
|
import { LEFT } from '../const'
|
|
|
|
|
|
import type { Topic, Wrapper, Parent, Children, Expander } from '../types/dom'
|
|
|
|
|
|
import type { MindElixirInstance, NodeObj } from '../types/index'
|
|
|
|
|
|
import { encodeHTML, getOffsetLT } from '../utils/index'
|
|
|
|
|
|
import { layoutChildren } from './layout'
|
|
|
|
|
|
import { marked } from 'marked'
|
|
|
|
|
|
// 移除imageProcessor引用,使用MindElixir原生image属性
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// DOM manipulation
|
|
|
|
|
|
const $d = document
|
|
|
|
|
|
export const findEle = function (this: MindElixirInstance, id: string, el?: HTMLElement) {
|
|
|
|
|
|
const scope = this?.el ? this.el : el ? el : document
|
|
|
|
|
|
const ele = scope.querySelector<Topic>(`[data-nodeid="me${id}"]`)
|
|
|
|
|
|
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 = ''
|
|
|
|
|
|
|
|
|
|
|
|
if (nodeObj.style) {
|
|
|
|
|
|
const style = nodeObj.style
|
|
|
|
|
|
type KeyOfStyle = keyof typeof style
|
|
|
|
|
|
for (const key in style) {
|
|
|
|
|
|
tpc.style[key as KeyOfStyle] = style[key as KeyOfStyle]!
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (nodeObj.dangerouslySetInnerHTML) {
|
|
|
|
|
|
// 清理HTML内容,移除重复的格式
|
|
|
|
|
|
let cleanedHTML = nodeObj.dangerouslySetInnerHTML
|
|
|
|
|
|
|
|
|
|
|
|
// 检查是否包含表格,如果包含表格则不进行列表处理
|
|
|
|
|
|
const hasTable = cleanedHTML.includes('<table') || cleanedHTML.includes('<td>') || cleanedHTML.includes('<th>')
|
|
|
|
|
|
|
|
|
|
|
|
if (!hasTable) {
|
|
|
|
|
|
// 移除• 【这种重复格式
|
|
|
|
|
|
cleanedHTML = cleanedHTML.replace(/•\s*【/g, '【')
|
|
|
|
|
|
cleanedHTML = cleanedHTML.replace(/•\s*\[/g, '[')
|
|
|
|
|
|
|
|
|
|
|
|
// 移除其他可能的重复格式
|
|
|
|
|
|
cleanedHTML = cleanedHTML.replace(/•\s*(/g, '(')
|
|
|
|
|
|
cleanedHTML = cleanedHTML.replace(/•\s*\(/g, '(')
|
|
|
|
|
|
|
|
|
|
|
|
// 处理换行符和列表格式
|
|
|
|
|
|
// 将换行符转换为<br>标签,但保持列表结构
|
|
|
|
|
|
cleanedHTML = cleanedHTML
|
|
|
|
|
|
// 处理列表项:将每个•开头的行转换为<li>
|
|
|
|
|
|
.replace(/^(\s*)•\s*(.+)$/gm, '<li>$2</li>')
|
|
|
|
|
|
// 将连续的<li>标签包装在<ul>中
|
|
|
|
|
|
.replace(/(<li>.*<\/li>)/gs, (match) => {
|
|
|
|
|
|
// 如果已经被<ul>包装,不重复包装
|
|
|
|
|
|
if (cleanedHTML.includes('<ul>')) return match
|
|
|
|
|
|
return `<ul>${match}</ul>`
|
|
|
|
|
|
})
|
|
|
|
|
|
// 将剩余的换行符转换为<br>
|
|
|
|
|
|
.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, '(')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
tpc.innerHTML = cleanedHTML
|
|
|
|
|
|
|
|
|
|
|
|
// 检查是否包含图片,决定文本对齐方式
|
|
|
|
|
|
const hasImages = cleanedHTML.includes('<img')
|
|
|
|
|
|
const hasNodeImage = !!nodeObj.image
|
|
|
|
|
|
const hasAnyImage = hasImages || hasNodeImage
|
|
|
|
|
|
|
|
|
|
|
|
// 统一使用左对齐样式,简化逻辑
|
|
|
|
|
|
tpc.classList.add('no-image')
|
|
|
|
|
|
tpc.classList.remove('has-image')
|
|
|
|
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (nodeObj.image) {
|
|
|
|
|
|
const img = nodeObj.image
|
|
|
|
|
|
if (img.url && img.width && img.height) {
|
|
|
|
|
|
const imgEl = $d.createElement('img')
|
|
|
|
|
|
// Use imageProxy function if provided, otherwise use original URL
|
|
|
|
|
|
imgEl.src = this.imageProxy ? this.imageProxy(img.url) : img.url
|
|
|
|
|
|
imgEl.style.width = img.width + 'px'
|
|
|
|
|
|
imgEl.style.height = img.height + 'px'
|
|
|
|
|
|
if (img.fit) imgEl.style.objectFit = img.fit
|
|
|
|
|
|
tpc.appendChild(imgEl)
|
|
|
|
|
|
tpc.image = imgEl
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.warn('Image url/width/height are required')
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if (tpc.image) {
|
|
|
|
|
|
tpc.image = undefined
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
const textEl = $d.createElement('span')
|
|
|
|
|
|
textEl.className = 'text'
|
|
|
|
|
|
|
|
|
|
|
|
// Check if markdown parser is provided and topic contains markdown syntax
|
|
|
|
|
|
let content = ''
|
|
|
|
|
|
if (this.markdown) {
|
|
|
|
|
|
content = this.markdown(nodeObj.topic, nodeObj)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 直接设置文本内容,图片通过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, '(')
|
|
|
|
|
|
|
|
|
|
|
|
textEl.innerHTML = content
|
|
|
|
|
|
tpc.appendChild(textEl)
|
|
|
|
|
|
tpc.text = textEl
|
|
|
|
|
|
|
|
|
|
|
|
// 检查是否有图片,决定文本对齐方式
|
|
|
|
|
|
const hasNodeImage = !!nodeObj.image
|
|
|
|
|
|
const hasImageInText = content.includes('<img')
|
|
|
|
|
|
const hasAnyImage = hasNodeImage || hasImageInText
|
|
|
|
|
|
|
|
|
|
|
|
// 统一使用左对齐样式,简化逻辑
|
|
|
|
|
|
tpc.classList.add('no-image')
|
|
|
|
|
|
tpc.classList.remove('has-image')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (nodeObj.hyperLink) {
|
|
|
|
|
|
const linkEl = $d.createElement('a')
|
|
|
|
|
|
linkEl.className = 'hyper-link'
|
|
|
|
|
|
linkEl.target = '_blank'
|
|
|
|
|
|
linkEl.innerText = '🔗'
|
|
|
|
|
|
linkEl.href = nodeObj.hyperLink
|
|
|
|
|
|
tpc.appendChild(linkEl)
|
|
|
|
|
|
tpc.link = linkEl
|
|
|
|
|
|
} else if (tpc.link) {
|
|
|
|
|
|
tpc.link = undefined
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (nodeObj.icons && nodeObj.icons.length) {
|
|
|
|
|
|
const iconsEl = $d.createElement('span')
|
|
|
|
|
|
iconsEl.className = 'icons'
|
|
|
|
|
|
iconsEl.innerHTML = nodeObj.icons.map(icon => `<span>${encodeHTML(icon)}</span>`).join('')
|
|
|
|
|
|
tpc.appendChild(iconsEl)
|
|
|
|
|
|
tpc.icons = iconsEl
|
|
|
|
|
|
} else if (tpc.icons) {
|
|
|
|
|
|
tpc.icons = undefined
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (nodeObj.tags && nodeObj.tags.length) {
|
|
|
|
|
|
const tagsEl = $d.createElement('div')
|
|
|
|
|
|
tagsEl.className = 'tags'
|
|
|
|
|
|
|
|
|
|
|
|
nodeObj.tags.forEach(tag => {
|
|
|
|
|
|
const span = $d.createElement('span')
|
|
|
|
|
|
|
|
|
|
|
|
if (typeof tag === 'string') {
|
|
|
|
|
|
span.textContent = tag
|
|
|
|
|
|
} else {
|
|
|
|
|
|
span.textContent = tag.text
|
|
|
|
|
|
if (tag.className) {
|
|
|
|
|
|
span.className = tag.className
|
|
|
|
|
|
}
|
|
|
|
|
|
if (tag.style) {
|
|
|
|
|
|
Object.assign(span.style, tag.style)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
tagsEl.appendChild(span)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
tpc.appendChild(tagsEl)
|
|
|
|
|
|
tpc.tags = tagsEl
|
|
|
|
|
|
} else if (tpc.tags) {
|
|
|
|
|
|
tpc.tags = undefined
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// everything start from `Wrapper`
|
|
|
|
|
|
export const createWrapper = function (this: MindElixirInstance, nodeObj: NodeObj, omitChildren?: boolean) {
|
|
|
|
|
|
const grp = $d.createElement('me-wrapper') as Wrapper
|
|
|
|
|
|
const { p, tpc } = this.createParent(nodeObj)
|
|
|
|
|
|
grp.appendChild(p)
|
|
|
|
|
|
if (!omitChildren && nodeObj.children && nodeObj.children.length > 0) {
|
|
|
|
|
|
const expander = createExpander(nodeObj.expanded)
|
|
|
|
|
|
p.appendChild(expander)
|
|
|
|
|
|
// tpc.expander = expander
|
|
|
|
|
|
if (nodeObj.expanded !== false) {
|
|
|
|
|
|
const children = layoutChildren(this, nodeObj.children)
|
|
|
|
|
|
grp.appendChild(children)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return { grp, top: p, tpc }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export const createParent = function (this: MindElixirInstance, nodeObj: NodeObj) {
|
|
|
|
|
|
const p = $d.createElement('me-parent') as Parent
|
|
|
|
|
|
const tpc = this.createTopic(nodeObj)
|
|
|
|
|
|
shapeTpc.call(this, tpc, nodeObj)
|
|
|
|
|
|
p.appendChild(tpc)
|
|
|
|
|
|
|
|
|
|
|
|
return { p, tpc }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const createChildren = function (this: MindElixirInstance, wrappers: Wrapper[]) {
|
|
|
|
|
|
const children = $d.createElement('me-children') as Children
|
|
|
|
|
|
children.append(...wrappers)
|
|
|
|
|
|
return children
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export const createTopic = function (this: MindElixirInstance, nodeObj: NodeObj) {
|
|
|
|
|
|
const topic = $d.createElement('me-tpc') as Topic
|
|
|
|
|
|
topic.nodeObj = nodeObj
|
|
|
|
|
|
topic.dataset.nodeid = 'me' + nodeObj.id
|
|
|
|
|
|
topic.draggable = this.draggable
|
|
|
|
|
|
return topic
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function selectText(div: HTMLElement) {
|
|
|
|
|
|
const range = $d.createRange()
|
|
|
|
|
|
range.selectNodeContents(div)
|
|
|
|
|
|
const getSelection = window.getSelection()
|
|
|
|
|
|
if (getSelection) {
|
|
|
|
|
|
getSelection.removeAllRanges()
|
|
|
|
|
|
getSelection.addRange(range)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export const editTopic = function (this: MindElixirInstance, el: Topic) {
|
|
|
|
|
|
console.time('editTopic')
|
|
|
|
|
|
if (!el) return
|
|
|
|
|
|
const div = $d.createElement('div')
|
|
|
|
|
|
const node = el.nodeObj
|
|
|
|
|
|
|
|
|
|
|
|
// Get the original content from topic
|
|
|
|
|
|
const originalContent = node.topic
|
|
|
|
|
|
|
|
|
|
|
|
el.appendChild(div)
|
|
|
|
|
|
div.id = 'input-box'
|
|
|
|
|
|
div.textContent = originalContent
|
|
|
|
|
|
div.contentEditable = 'plaintext-only'
|
|
|
|
|
|
div.spellcheck = false
|
|
|
|
|
|
const style = getComputedStyle(el)
|
|
|
|
|
|
div.style.cssText = `min-width:${el.offsetWidth - 8}px;
|
|
|
|
|
|
color:${style.color};
|
|
|
|
|
|
padding:${style.padding};
|
|
|
|
|
|
margin:${style.margin};
|
|
|
|
|
|
font:${style.font};
|
|
|
|
|
|
background-color:${style.backgroundColor !== 'rgba(0, 0, 0, 0)' && style.backgroundColor};
|
|
|
|
|
|
border-radius:${style.borderRadius};`
|
|
|
|
|
|
if (this.direction === LEFT) div.style.right = '0'
|
|
|
|
|
|
|
|
|
|
|
|
selectText(div)
|
|
|
|
|
|
|
|
|
|
|
|
this.bus.fire('operation', {
|
|
|
|
|
|
name: 'beginEdit',
|
|
|
|
|
|
obj: el.nodeObj,
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
div.addEventListener('keydown', e => {
|
|
|
|
|
|
e.stopPropagation()
|
|
|
|
|
|
const key = e.key
|
|
|
|
|
|
|
|
|
|
|
|
if (key === 'Enter' || key === 'Tab') {
|
|
|
|
|
|
// keep wrap for shift enter
|
|
|
|
|
|
if (e.shiftKey) return
|
|
|
|
|
|
|
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
|
div.blur()
|
|
|
|
|
|
this.container.focus()
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
div.addEventListener('blur', () => {
|
|
|
|
|
|
if (!div) return
|
|
|
|
|
|
const inputContent = div.textContent?.trim() || ''
|
|
|
|
|
|
|
|
|
|
|
|
if (inputContent === '') {
|
|
|
|
|
|
node.topic = originalContent
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Update topic content
|
|
|
|
|
|
node.topic = inputContent
|
|
|
|
|
|
|
|
|
|
|
|
if (this.markdown) {
|
|
|
|
|
|
el.text.innerHTML = this.markdown(node.topic, node)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Plain text content
|
|
|
|
|
|
el.text.textContent = inputContent
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
div.remove()
|
|
|
|
|
|
|
|
|
|
|
|
// 重新应用样式和清理逻辑
|
|
|
|
|
|
shapeTpc.call(this, el, node)
|
|
|
|
|
|
|
|
|
|
|
|
if (inputContent === originalContent) return
|
|
|
|
|
|
|
|
|
|
|
|
this.linkDiv()
|
|
|
|
|
|
this.bus.fire('operation', {
|
|
|
|
|
|
name: 'finishEdit',
|
|
|
|
|
|
obj: node,
|
|
|
|
|
|
origin: originalContent,
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
console.timeEnd('editTopic')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export const createExpander = function (expanded: boolean | undefined): Expander {
|
|
|
|
|
|
const expander = $d.createElement('me-epd') as Expander
|
|
|
|
|
|
// if expanded is undefined, treat as expanded
|
|
|
|
|
|
expander.expanded = expanded !== false
|
|
|
|
|
|
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;
|
2025-10-11 06:10:30 +00:00
|
|
|
|
border: 2px solid #660874;
|
2025-10-11 06:07:16 +00:00
|
|
|
|
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;
|
2025-10-11 06:10:30 +00:00
|
|
|
|
caret-color: #660874;
|
2025-10-11 06:07:16 +00:00
|
|
|
|
`
|
|
|
|
|
|
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;
|
2025-10-11 06:10:30 +00:00
|
|
|
|
background: #660874;
|
2025-10-11 06:07:16 +00:00
|
|
|
|
color: white;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
transition: background 0.2s;
|
|
|
|
|
|
`
|
|
|
|
|
|
saveButton.addEventListener('mouseenter', () => {
|
2025-10-11 06:10:30 +00:00
|
|
|
|
saveButton.style.background = '#4d0655'
|
2025-10-11 06:07:16 +00:00
|
|
|
|
})
|
|
|
|
|
|
saveButton.addEventListener('mouseleave', () => {
|
2025-10-11 06:10:30 +00:00
|
|
|
|
saveButton.style.background = '#660874'
|
2025-10-11 06:07:16 +00:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
2025-10-11 07:06:38 +00:00
|
|
|
|
// 触发操作历史记录,确保数据持久化
|
|
|
|
|
|
this.bus.fire('operation', {
|
|
|
|
|
|
name: 'finishEditTable',
|
|
|
|
|
|
obj: node,
|
|
|
|
|
|
origin: originalHTML,
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2025-10-11 06:07:16 +00:00
|
|
|
|
// 添加样式类
|
|
|
|
|
|
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()
|
|
|
|
|
|
}
|