Commit f7d98cfa by pangchong

refactor(editor): 优化 XML 编辑器文本节点相关交互与显示

- 修改双击词选中逻辑,统一选中完整单词的两端
- 序列化函数支持文本节点的正确转义与输出
- 右键菜单目标节点项增加 tooltip 显示完整内容,防止截断
- 智能翻译判定规则调整,精确匹配中文文本与对应英文原文
- 复制节点逻辑支持文本节点复制,提示和复制内容改进
- 粘贴文本节点实现多场景插入,包括混排内容和虚拟节点
- 节点显示名称增加 tooltip 字段,显示原始完整文本
- 面包屑路径生成时,单段文本不单列节点,仅多段时展示 #text 级别
- 编辑器面板复制按钮文本区分普通节点和文本节点显示“复制文本”
parent 1815f01c
...@@ -1069,21 +1069,14 @@ const initCodeMirror = () => { ...@@ -1069,21 +1069,14 @@ const initCodeMirror = () => {
// 检查双击点是否在属性区(tagText 包含空格、= 或引号) // 检查双击点是否在属性区(tagText 包含空格、= 或引号)
const isInsideAttribute = /[\s='"]/.test(tagText) const isInsideAttribute = /[\s='"]/.test(tagText)
let checkPos = clickPos // 无论双击的是属性名、属性值还是标签名,均向两侧扩展选中完整单词
if (isInsideAttribute) { let wordStart = clickPos
// 1. 双击属性名/属性值:定位到词首,前缀为空 -> 展示全量候选列表 let wordEnd = clickPos
while (checkPos > 0 && /[a-zA-Z0-9_-]/.test(docText[checkPos - 1])) { while (wordStart > 0 && /[a-zA-Z0-9_-]/.test(docText[wordStart - 1])) wordStart--
checkPos-- while (wordEnd < docText.length && /[a-zA-Z0-9_-]/.test(docText[wordEnd])) wordEnd++
}
} else { // 选中整个词,光标停在词尾(CodeMirror anchor=start, head=end 即左→右高亮)
// 2. 双击节点标签名:定位到词尾,带有完整单词前缀 -> 进行完全匹配 editorView.dispatch({ selection: { anchor: wordStart, head: wordEnd } })
while (checkPos < docText.length && /[a-zA-Z0-9_-]/.test(docText[checkPos])) {
checkPos++
}
}
// 折叠光标到目标位置
editorView.dispatch({ selection: { anchor: checkPos } })
editorView.focus() editorView.focus()
setTimeout(() => { setTimeout(() => {
......
...@@ -119,6 +119,9 @@ function domElementToXmlNode(element: Element, parentId: string | null): XmlNode ...@@ -119,6 +119,9 @@ function domElementToXmlNode(element: Element, parentId: string | null): XmlNode
* 将 XmlNode 树序列化回 XML 字符串 * 将 XmlNode 树序列化回 XML 字符串
*/ */
export function serializeTreeToXml(node: XmlNode, indent: number = 0, compact: boolean = false): string { export function serializeTreeToXml(node: XmlNode, indent: number = 0, compact: boolean = false): string {
if (node.tagName === '#text') {
return escapeXmlText(node.textContent || '')
}
const pad = compact ? '' : ' '.repeat(indent) const pad = compact ? '' : ' '.repeat(indent)
const newline = compact ? '' : '\n' const newline = compact ? '' : '\n'
const attrs = Object.entries(node.attributes) const attrs = Object.entries(node.attributes)
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
class="target-node-item" class="target-node-item"
:class="{ 'is-selected': isSelected }" :class="{ 'is-selected': isSelected }"
:style="{ paddingLeft: `${(item.depth || 0) * 12 + 12}px` }" :style="{ paddingLeft: `${(item.depth || 0) * 12 + 12}px` }"
:title="item.displayName" :title="item.tooltip || item.displayName"
> >
<span class="check-space"> <span class="check-space">
<span v-if="isSelected" class="check-icon"></span> <span v-if="isSelected" class="check-icon"></span>
...@@ -19,6 +19,8 @@ export interface TargetItem { ...@@ -19,6 +19,8 @@ export interface TargetItem {
tagName: string tagName: string
suffix: string suffix: string
displayName: string displayName: string
/** 完整未截断的内容,供原生 title 属性悬停时展示 */
tooltip?: string
pathString: string pathString: string
depth: number depth: number
} }
......
...@@ -34,15 +34,22 @@ export function useEditorPanel() { ...@@ -34,15 +34,22 @@ export function useEditorPanel() {
curr = curr.parent ? editorStore.nodeMap.get(curr.parent.id) : undefined curr = curr.parent ? editorStore.nodeMap.get(curr.parent.id) : undefined
} }
if (editorStore.selectedNodeId.includes('-txt-')) { if (editorStore.selectedNodeId.includes('-txt-')) {
path.push({ // 当父节点 mixedContent 中只有一段文本时(singleTextMode),
id: editorStore.selectedNodeId, // 该文本被视为父节点自身内容,不在树中单独展示,面包屑也不追加 #text 层级。
tagName: '#text', // 只有多段文本(多个独立 #text 虚拟节点)时,才需要将当前 #text 加入路径。
attributes: {}, const parentNode = editorStore.nodeMap.get(realId)?.node
children: [], const textItemCount = parentNode?.mixedContent?.filter((m) => m.type === 'text' && (m.text || '').trim()).length ?? 0
textContent: '', if (textItemCount > 1) {
mixedContent: [], path.push({
parentId: realId id: editorStore.selectedNodeId,
}) tagName: '#text',
attributes: {},
children: [],
textContent: '',
mixedContent: [],
parentId: realId
})
}
} }
return path return path
}) })
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment