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(`[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('') || cleanedHTML.includes('') 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*)•\s*(.+)$/gm, '
  • $2
  • ') // 将连续的
  • 标签包装在
      中 .replace(/(
    • .*<\/li>)/gs, (match) => { // 如果已经被
        包装,不重复包装 if (cleanedHTML.includes('
          ')) return match return `
            ${match}
          ` }) // 将剩余的换行符转换为
          .replace(/\n/g, '
          ') } 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(' `${encodeHTML(icon)}`).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; 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(//g, '
          ') 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() }