修复思维导图文本对齐和格式清理问题
- 实现智能文本对齐:纯文本节点左对齐,带图片节点居中 - 自动清理重复格式标记:移除• 【等重复格式 - 修复双击编辑后样式恢复问题:编辑完成后重新应用样式逻辑 - 优化CSS类名控制:使用.no-image和.has-image类动态控制对齐方式 - 确保SVG导出和显示效果一致性
This commit is contained in:
parent
a8051a50e8
commit
cd9b1f5a38
|
|
@ -235,11 +235,61 @@
|
|||
max-width: 35em;
|
||||
white-space: pre-wrap;
|
||||
pointer-events: all;
|
||||
// 确保整个节点内容居中对齐
|
||||
// 默认居中对齐,但会根据是否有图片动态调整
|
||||
text-align: center !important;
|
||||
flex-direction: column !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
|
||||
// 没有图片的节点使用左对齐
|
||||
&.no-image {
|
||||
text-align: left !important;
|
||||
align-items: flex-start !important;
|
||||
|
||||
.text {
|
||||
text-align: left !important;
|
||||
}
|
||||
|
||||
ul, ol {
|
||||
text-align: left !important;
|
||||
padding-left: 20px !important;
|
||||
list-style-position: outside !important;
|
||||
}
|
||||
|
||||
li {
|
||||
text-align: left !important;
|
||||
list-style-position: outside !important;
|
||||
}
|
||||
|
||||
p, span, div, strong, em {
|
||||
text-align: left !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 有图片的节点保持居中对齐
|
||||
&.has-image {
|
||||
text-align: center !important;
|
||||
align-items: center !important;
|
||||
|
||||
.text {
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
ul, ol {
|
||||
text-align: center !important;
|
||||
padding-left: 0 !important;
|
||||
list-style-position: inside !important;
|
||||
}
|
||||
|
||||
li {
|
||||
text-align: center !important;
|
||||
list-style-position: inside !important;
|
||||
}
|
||||
|
||||
p, span, div, strong, em {
|
||||
text-align: center !important;
|
||||
}
|
||||
}
|
||||
box-sizing: border-box !important;
|
||||
// 确保节点有固定的最小高度,避免内容变化导致尺寸不稳定
|
||||
min-height: 2em !important;
|
||||
|
|
@ -260,7 +310,6 @@
|
|||
}
|
||||
& > .text {
|
||||
display: block !important;
|
||||
text-align: center !important; // 确保文本居中对齐,避免与连线错位
|
||||
width: 100% !important; // 确保文本容器占满节点宽度
|
||||
margin: 0 auto !important; // 强制居中
|
||||
// 确保文本不会导致节点尺寸不稳定
|
||||
|
|
|
|||
|
|
@ -35,16 +35,36 @@ function generateSvgText(tpc: HTMLElement, tpcStyle: CSSStyleDeclaration, x: num
|
|||
const paddingTop = parseInt(tpcStyle.paddingTop) || 8
|
||||
const paddingLeft = parseInt(tpcStyle.paddingLeft) || 8
|
||||
|
||||
// 计算节点宽度用于居中
|
||||
// 计算节点宽度和文本起始位置
|
||||
const nodeWidth = tpc.offsetWidth || 200
|
||||
const centerX = x + nodeWidth / 2
|
||||
const textX = x + paddingLeft // 纯文本节点使用左对齐
|
||||
|
||||
// 计算实际需要的内容高度
|
||||
const contentHeight = (lines.length - 1) * lineHeight + fontSize + paddingTop + (parseInt(tpcStyle.paddingBottom) || 8)
|
||||
const nodeHeight = tpc.offsetHeight || 100
|
||||
const actualHeight = Math.min(contentHeight, nodeHeight) // 不超过原始节点高度
|
||||
|
||||
// 创建背景矩形,使用计算出的实际高度
|
||||
const bg = document.createElementNS(ns, 'rect')
|
||||
setAttributes(bg, {
|
||||
x: x + '',
|
||||
y: y + '',
|
||||
rx: tpcStyle.borderRadius || '8',
|
||||
ry: tpcStyle.borderRadius || '8',
|
||||
width: nodeWidth + 'px',
|
||||
height: actualHeight + 'px',
|
||||
fill: tpcStyle.backgroundColor || 'white',
|
||||
stroke: tpcStyle.borderColor || '#ccc',
|
||||
'stroke-width': tpcStyle.borderWidth || '1',
|
||||
})
|
||||
g.appendChild(bg)
|
||||
|
||||
lines.forEach((line, index) => {
|
||||
const text = document.createElementNS(ns, 'text')
|
||||
setAttributes(text, {
|
||||
x: centerX + '',
|
||||
x: textX + '',
|
||||
y: y + paddingTop + fontSize + (lineHeight * index) + '',
|
||||
'text-anchor': 'middle',
|
||||
'text-anchor': 'start',
|
||||
'font-family': tpcStyle.fontFamily,
|
||||
'font-size': `${tpcStyle.fontSize}`,
|
||||
'font-weight': `${tpcStyle.fontWeight}`,
|
||||
|
|
@ -53,6 +73,13 @@ function generateSvgText(tpc: HTMLElement, tpcStyle: CSSStyleDeclaration, x: num
|
|||
text.innerHTML = line
|
||||
g.appendChild(text)
|
||||
})
|
||||
|
||||
console.log('✅ generateSvgText优化了底部间距:', {
|
||||
originalHeight: nodeHeight,
|
||||
actualHeight: actualHeight,
|
||||
linesCount: lines.length
|
||||
})
|
||||
|
||||
return g
|
||||
}
|
||||
|
||||
|
|
@ -331,6 +358,12 @@ function cleanHtmlForSvg(html: string): string {
|
|||
// 移除其他可能导致SVG解析错误的标签
|
||||
cleanedHtml = cleanedHtml.replace(/<br\s*\/?>/gi, ' ')
|
||||
|
||||
// 清理重复的格式标记
|
||||
cleanedHtml = cleanedHtml.replace(/•\s*【/g, '【')
|
||||
cleanedHtml = cleanedHtml.replace(/•\s*\[/g, '[')
|
||||
cleanedHtml = cleanedHtml.replace(/•\s*(/g, '(')
|
||||
cleanedHtml = cleanedHtml.replace(/•\s*\(/g, '(')
|
||||
|
||||
// 确保所有标签都正确闭合
|
||||
cleanedHtml = cleanedHtml.replace(/<([^>]+)>/g, (match, tagContent) => {
|
||||
// 检查是否是自闭合标签
|
||||
|
|
@ -403,12 +436,18 @@ function generateSvgTextUsingForeignObject(tpc: HTMLElement, tpcStyle: CSSStyleD
|
|||
const nodeWidth = rect.width
|
||||
const nodeHeight = rect.height
|
||||
|
||||
// 检查是否包含任何图片
|
||||
const hasImages = cleanedHtml.includes('<img')
|
||||
const hasNodeImage = tpcWithNodeObj.nodeObj && tpcWithNodeObj.nodeObj.image
|
||||
const hasAnyImage = hasImages || hasNodeImage
|
||||
|
||||
console.log('🔍 使用getBoundingClientRect获取尺寸:', {
|
||||
nodeWidth,
|
||||
nodeHeight,
|
||||
offsetWidth: tpc.offsetWidth,
|
||||
offsetHeight: tpc.offsetHeight,
|
||||
rect: rect
|
||||
rect: rect,
|
||||
hasAnyImage: hasAnyImage
|
||||
})
|
||||
|
||||
// 尝试使用原生SVG文本渲染
|
||||
|
|
@ -435,15 +474,25 @@ function generateSvgTextUsingForeignObject(tpc: HTMLElement, tpcStyle: CSSStyleD
|
|||
if (lines.length > 0) {
|
||||
const fontSize = parseFloat(tpcStyle.fontSize) || 14
|
||||
const lineHeight = fontSize * 1.4
|
||||
const centerX = x + nodeWidth / 2
|
||||
const startY = y + fontSize + (parseInt(tpcStyle.paddingTop) || 8)
|
||||
const paddingTop = parseInt(tpcStyle.paddingTop) || 8
|
||||
const paddingBottom = parseInt(tpcStyle.paddingBottom) || 8
|
||||
const paddingLeft = parseInt(tpcStyle.paddingLeft) || 8
|
||||
|
||||
// 计算实际需要的内容高度
|
||||
const contentHeight = (lines.length - 1) * lineHeight + fontSize + paddingTop + paddingBottom
|
||||
const actualHeight = Math.min(contentHeight, nodeHeight) // 不超过原始节点高度
|
||||
|
||||
// 检查是否包含图片
|
||||
const hasImages = cleanedHtml.includes('<img')
|
||||
const textX = hasImages ? x + nodeWidth / 2 : x + paddingLeft
|
||||
const startY = y + fontSize + paddingTop
|
||||
|
||||
lines.forEach((line, index) => {
|
||||
const text = document.createElementNS(ns, 'text')
|
||||
setAttributes(text, {
|
||||
x: centerX + '',
|
||||
x: textX + '',
|
||||
y: startY + (lineHeight * index) + '',
|
||||
'text-anchor': 'middle',
|
||||
'text-anchor': hasImages ? 'middle' : 'start',
|
||||
'font-family': tpcStyle.fontFamily,
|
||||
'font-size': tpcStyle.fontSize,
|
||||
'font-weight': tpcStyle.fontWeight,
|
||||
|
|
@ -453,8 +502,27 @@ function generateSvgTextUsingForeignObject(tpc: HTMLElement, tpcStyle: CSSStyleD
|
|||
g.appendChild(text)
|
||||
})
|
||||
|
||||
// 创建背景矩形,使用计算出的实际高度
|
||||
const bg = document.createElementNS(ns, 'rect')
|
||||
setAttributes(bg, {
|
||||
x: x + '',
|
||||
y: y + '',
|
||||
rx: tpcStyle.borderRadius || '8',
|
||||
ry: tpcStyle.borderRadius || '8',
|
||||
width: nodeWidth + 'px',
|
||||
height: actualHeight + 'px',
|
||||
fill: tpcStyle.backgroundColor || 'white',
|
||||
stroke: tpcStyle.borderColor || '#ccc',
|
||||
'stroke-width': tpcStyle.borderWidth || '1',
|
||||
})
|
||||
g.insertBefore(bg, g.firstChild) // 将背景放在最前面
|
||||
|
||||
document.body.removeChild(tempDiv)
|
||||
console.log('✅ 使用原生SVG文本渲染成功')
|
||||
console.log('✅ 使用原生SVG文本渲染成功,优化了底部间距:', {
|
||||
originalHeight: nodeHeight,
|
||||
actualHeight: actualHeight,
|
||||
linesCount: lines.length
|
||||
})
|
||||
return g
|
||||
}
|
||||
|
||||
|
|
@ -493,12 +561,12 @@ function generateSvgTextUsingForeignObject(tpc: HTMLElement, tpcStyle: CSSStyleD
|
|||
padding: ${paddingTop}px ${paddingRight}px ${paddingBottom}px ${paddingLeft}px;
|
||||
box-sizing: border-box;
|
||||
overflow: visible;
|
||||
text-align: center !important;
|
||||
text-align: ${hasAnyImage ? 'center' : 'left'} !important;
|
||||
line-height: 1.4;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
align-items: ${hasAnyImage ? 'center' : 'flex-start'};
|
||||
position: relative;
|
||||
`
|
||||
|
||||
|
|
@ -536,9 +604,9 @@ function generateSvgTextUsingForeignObject(tpc: HTMLElement, tpcStyle: CSSStyleD
|
|||
const htmlList = list as HTMLElement
|
||||
htmlList.style.cssText = `
|
||||
margin: 2px 0 4px 0 !important;
|
||||
padding-left: 0 !important;
|
||||
text-align: center !important;
|
||||
list-style-position: inside !important;
|
||||
padding-left: ${hasAnyImage ? '0' : '20px'} !important;
|
||||
text-align: ${hasAnyImage ? 'center' : 'left'} !important;
|
||||
list-style-position: ${hasAnyImage ? 'inside' : 'outside'} !important;
|
||||
`
|
||||
})
|
||||
|
||||
|
|
@ -549,12 +617,12 @@ function generateSvgTextUsingForeignObject(tpc: HTMLElement, tpcStyle: CSSStyleD
|
|||
margin: 1px 0 !important;
|
||||
line-height: 1.2 !important;
|
||||
padding: 0 !important;
|
||||
text-align: center !important;
|
||||
list-style-position: inside !important;
|
||||
text-align: ${hasAnyImage ? 'center' : 'left'} !important;
|
||||
list-style-position: ${hasAnyImage ? 'inside' : 'outside'} !important;
|
||||
`
|
||||
})
|
||||
|
||||
// 为段落文本添加居中样式
|
||||
// 为段落文本添加样式
|
||||
const paragraphs = div.querySelectorAll('p')
|
||||
paragraphs.forEach(p => {
|
||||
const htmlP = p as HTMLElement
|
||||
|
|
@ -562,27 +630,17 @@ function generateSvgTextUsingForeignObject(tpc: HTMLElement, tpcStyle: CSSStyleD
|
|||
margin: 2px 0 !important;
|
||||
line-height: 1.4 !important;
|
||||
padding: 0 !important;
|
||||
text-align: center !important;
|
||||
text-align: ${hasAnyImage ? 'center' : 'left'} !important;
|
||||
`
|
||||
})
|
||||
|
||||
// 为所有文本元素添加居中样式
|
||||
// 为所有文本元素添加样式
|
||||
const textElements = div.querySelectorAll('span, div, strong, em')
|
||||
textElements.forEach(element => {
|
||||
const htmlElement = element as HTMLElement
|
||||
htmlElement.style.textAlign = 'center !important'
|
||||
htmlElement.style.textAlign = `${hasAnyImage ? 'center' : 'left'} !important`
|
||||
})
|
||||
|
||||
// 添加全局CSS样式确保所有内容居中
|
||||
const style = document.createElement('style')
|
||||
style.textContent = `
|
||||
* {
|
||||
text-align: center !important;
|
||||
margin-left: auto !important;
|
||||
margin-right: auto !important;
|
||||
}
|
||||
`
|
||||
div.appendChild(style)
|
||||
|
||||
// 为图片元素添加特殊样式处理,彻底解决图片压线问题
|
||||
const images = div.querySelectorAll('img')
|
||||
|
|
@ -637,11 +695,12 @@ function createElBox(mei: MindElixirInstance, tpc: Topic) {
|
|||
const tpcStyle = getComputedStyle(tpc)
|
||||
const { offsetLeft: x, offsetTop: y } = getOffsetLT(mei.nodes, tpc)
|
||||
|
||||
// 检查是否有HTML内容或节点图片,如果有则增加高度
|
||||
const tpcWithNodeObj = tpc as Topic
|
||||
const hasHTMLContent = tpcWithNodeObj.nodeObj && tpcWithNodeObj.nodeObj.dangerouslySetInnerHTML
|
||||
const hasImages = hasHTMLContent && tpcWithNodeObj.nodeObj.dangerouslySetInnerHTML?.includes('<img')
|
||||
const hasNodeImage = tpcWithNodeObj.nodeObj && tpcWithNodeObj.nodeObj.image
|
||||
// 检查是否有HTML内容或节点图片,如果有则增加高度
|
||||
const tpcWithNodeObj = tpc as Topic
|
||||
const hasHTMLContent = tpcWithNodeObj.nodeObj && tpcWithNodeObj.nodeObj.dangerouslySetInnerHTML
|
||||
const hasImages = hasHTMLContent && tpcWithNodeObj.nodeObj.dangerouslySetInnerHTML?.includes('<img')
|
||||
const hasNodeImage = tpcWithNodeObj.nodeObj && tpcWithNodeObj.nodeObj.image
|
||||
const hasAnyImage = hasImages || hasNodeImage // 判断是否包含任何图片
|
||||
|
||||
const bg = document.createElementNS(ns, 'rect')
|
||||
const rect = tpc.getBoundingClientRect()
|
||||
|
|
|
|||
|
|
@ -26,7 +26,33 @@ export const shapeTpc = function (this: MindElixirInstance, tpc: Topic, nodeObj:
|
|||
}
|
||||
|
||||
if (nodeObj.dangerouslySetInnerHTML) {
|
||||
tpc.innerHTML = nodeObj.dangerouslySetInnerHTML
|
||||
// 清理HTML内容,移除重复的格式
|
||||
let cleanedHTML = nodeObj.dangerouslySetInnerHTML
|
||||
|
||||
// 移除• 【这种重复格式
|
||||
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
|
||||
|
||||
// 应用智能文本对齐样式
|
||||
if (hasAnyImage) {
|
||||
tpc.classList.add('has-image')
|
||||
tpc.classList.remove('no-image')
|
||||
} else {
|
||||
tpc.classList.add('no-image')
|
||||
tpc.classList.remove('has-image')
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -53,15 +79,37 @@ export const shapeTpc = function (this: MindElixirInstance, tpc: Topic, nodeObj:
|
|||
textEl.className = 'text'
|
||||
|
||||
// Check if markdown parser is provided and topic contains markdown syntax
|
||||
let content = ''
|
||||
if (this.markdown) {
|
||||
textEl.innerHTML = this.markdown(nodeObj.topic, nodeObj)
|
||||
content = this.markdown(nodeObj.topic, nodeObj)
|
||||
} else {
|
||||
// 直接设置文本内容,图片通过MindElixir原生image属性处理
|
||||
textEl.innerHTML = nodeObj.topic || ''
|
||||
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
|
||||
|
||||
// 应用智能文本对齐样式
|
||||
if (hasAnyImage) {
|
||||
tpc.classList.add('has-image')
|
||||
tpc.classList.remove('no-image')
|
||||
} else {
|
||||
tpc.classList.add('no-image')
|
||||
tpc.classList.remove('has-image')
|
||||
}
|
||||
}
|
||||
|
||||
if (nodeObj.hyperLink) {
|
||||
|
|
@ -229,6 +277,9 @@ export const editTopic = function (this: MindElixirInstance, el: Topic) {
|
|||
|
||||
div.remove()
|
||||
|
||||
// 重新应用样式和清理逻辑
|
||||
shapeTpc.call(this, el, node)
|
||||
|
||||
if (inputContent === originalContent) return
|
||||
|
||||
this.linkDiv()
|
||||
|
|
|
|||
Loading…
Reference in New Issue