Commit a43883bf by pangchong

feat(editor): 支持虚拟文本节点的编辑和操作

- getNodeDisplayName 函数新增对虚拟文本节点的文本内容预览显示支持,限制最大长度180字符
- 节点列表收集逻辑中过滤虚拟文本节点并正确展示顺序,增强节点展示准确性
- 编辑器中混合内容渲染增加虚拟文本节点的点击选中功能,支持对子文本内容的单独操作
- 编辑面板处理选中虚拟文本节点时,调整允许操作的标签类型为 '#text'
- 右键菜单新增对虚拟文本节点的编辑、删除、移动等操作支持,删除时同步更新混合内容数组并重建节点映射
- 编辑面板新增对虚拟文本节点的上移、下移操作,保持活动选中状态同步更新为对应的虚拟节点ID
- 针对文本节点特殊处理编辑结构和剪贴板相关选项,优化交互体验
- 节点添加时优先提取混合内容中第一个文本内容作为节点文本内容显示和编辑基础,提高显示准确性
parent dce75aea
...@@ -169,11 +169,27 @@ const getHighlightedHtml = (text: string) => { ...@@ -169,11 +169,27 @@ const getHighlightedHtml = (text: string) => {
} }
// ── 格式化节点显示文本 ── // ── 格式化节点显示文本 ──
const getNodeDisplayName = (node: XmlNode): string => { const getNodeDisplayName = (node: XmlNode, rawId?: string): string => {
if (rawId && rawId.includes('-txt-')) {
const realParentId = rawId.split('-txt-')[0]
const textIdx = parseInt(rawId.split('-txt-')[1], 10)
const parentItem = editorStore.nodeMap.get(realParentId)
if (parentItem && Array.isArray(parentItem.node.mixedContent)) {
let mcItem = parentItem.node.mixedContent[textIdx] as any
if (!mcItem || mcItem.type !== 'text') {
mcItem = parentItem.node.mixedContent.find((m: any) => m.type === 'text')
}
const text = mcItem?.text?.trim() || ''
const preview = text.length > 180 ? text.substring(0, 180) + '...' : text
return `文本内容 "${preview}"`
}
}
let suffix = '' let suffix = ''
if (node.tagName === '#text') { if (node.tagName === '#text') {
const text = node.textContent?.trim() || '' const text = node.textContent?.trim() || ''
suffix = ` "${text}"` const preview = text.length > 180 ? text.substring(0, 180) + '...' : text
suffix = ` "${preview}"`
return `文本内容${suffix}` return `文本内容${suffix}`
} }
...@@ -184,7 +200,8 @@ const getNodeDisplayName = (node: XmlNode): string => { ...@@ -184,7 +200,8 @@ const getNodeDisplayName = (node: XmlNode): string => {
} else { } else {
const text = node.textContent?.trim() || '' const text = node.textContent?.trim() || ''
if (text) { if (text) {
suffix = ` "${text}"` const preview = text.length > 180 ? text.substring(0, 180) + '...' : text
suffix = ` "${preview}"`
} }
} }
return `<${node.tagName}>${suffix}` return `<${node.tagName}>${suffix}`
...@@ -273,22 +290,18 @@ const processedItems = computed(() => { ...@@ -273,22 +290,18 @@ const processedItems = computed(() => {
const list: any[] = [] const list: any[] = []
// 🟢 A. 收集三代父辈节点 (自顶向下排列:曾爷爷 -> 爷爷 -> 爸爸,置于最顶层) // 🟢 A. 收集三代父辈节点 (自顶向下排列:曾爷爷 -> 爷爷 -> 爸爸,置于最顶层)
// props.nodeIds 包含 [自身, 爸爸, 爷爷, 曾爷爷, ...] const ancestorIds = props.nodeIds
// 我们取 index 在 1 到 5 之间的最近四代父辈并反转,呈自顶向下顺序,避免追溯到更顶层无关的根节点 .filter((id) => !id.includes('-txt-'))
// 四代父辈,自身,三代子辈,同辈 .slice(1, 5)
const ancestorIds = props.nodeIds.slice(1, 5).reverse() .reverse()
ancestorIds.forEach((id) => { ancestorIds.forEach((id) => {
let rid = id if (id !== baseRealId) {
if (id.includes('-txt-')) { const mapped = nodeMap.get(id)
rid = id.split('-txt-')[0]
}
if (rid !== baseRealId) {
const mapped = nodeMap.get(rid)
if (mapped) { if (mapped) {
const node = mapped.node const node = mapped.node
list.push({ list.push({
id: id, id: id,
displayName: getNodeDisplayName(node), displayName: getNodeDisplayName(node, id),
pathString: mapped.parent ? getNodeParentPath(mapped.parent.id, nodeMap) : '无 (根节点)', pathString: mapped.parent ? getNodeParentPath(mapped.parent.id, nodeMap) : '无 (根节点)',
checked: false, checked: false,
disabled: false disabled: false
...@@ -297,7 +310,7 @@ const processedItems = computed(() => { ...@@ -297,7 +310,7 @@ const processedItems = computed(() => {
} }
}) })
// 🟢 B. 添加自身 // 🟢 B. 添加基础真实元素节点 (如 <PARAC>)
list.push({ list.push({
id: baseRealId, id: baseRealId,
displayName: getNodeDisplayName(baseNode), displayName: getNodeDisplayName(baseNode),
...@@ -369,32 +382,51 @@ const processedItems = computed(() => { ...@@ -369,32 +382,51 @@ const processedItems = computed(() => {
} }
} }
// 🟢 F. 虚拟文本节点还原与精确定位插入 // 🟢 F. 虚拟文本节点还原与精确定位插入(根据 mixedContent 实时真实索引查找)
if (leafId.includes('-txt-')) { if (baseNode.mixedContent && baseNode.mixedContent.length > 0) {
const textIdx = parseInt(leafId.split('-txt-')[1], 10) let actualMcIdx = -1
const textVal = baseNode.mixedContent?.[textIdx]?.text || '' let textVal = ''
const virtualNode = {
id: leafId, const targetVirtualId = (props.selectedId && props.selectedId.includes('-txt-'))
tagName: '#text', ? props.selectedId
attributes: {}, : (leafId.includes('-txt-') ? leafId : '')
children: [],
textContent: textVal, if (targetVirtualId.includes('-txt-')) {
mixedContent: [], const targetIdx = parseInt(targetVirtualId.split('-txt-')[1], 10)
parentId: baseRealId if (baseNode.mixedContent[targetIdx]?.type === 'text') {
actualMcIdx = targetIdx
textVal = baseNode.mixedContent[targetIdx].text || ''
}
} }
const vItem = {
id: leafId, if (actualMcIdx === -1) {
displayName: getNodeDisplayName(virtualNode), actualMcIdx = baseNode.mixedContent.findIndex((m: any) => m.type === 'text' && (m.text || '').trim())
pathString: getNodeParentPath(baseRealId, nodeMap), if (actualMcIdx !== -1) {
checked: false, textVal = baseNode.mixedContent[actualMcIdx].text || ''
disabled: false }
} }
const parentIdx = list.findIndex((item) => item.id === baseRealId) if (actualMcIdx !== -1) {
if (parentIdx !== -1) { const liveVirtualId = `${baseRealId}-txt-${actualMcIdx}`
list.splice(parentIdx + 1, 0, vItem) const trimmed = textVal.trim()
} else { const preview = trimmed.length > 180 ? trimmed.substring(0, 180) + '...' : trimmed
list.push(vItem)
const vItem = {
id: liveVirtualId,
displayName: `文本内容 "${preview}"`,
pathString: getNodeParentPath(baseRealId, nodeMap),
checked: false,
disabled: false
}
if (!list.some((item) => item.id === liveVirtualId)) {
const parentIdx = list.findIndex((item) => item.id === baseRealId)
if (parentIdx !== -1) {
list.splice(parentIdx + 1, 0, vItem)
} else {
list.push(vItem)
}
}
} }
} }
......
...@@ -1010,7 +1010,12 @@ ...@@ -1010,7 +1010,12 @@
@click.stop="editorStore.setSelectedNodeId(node.id)" @click.stop="editorStore.setSelectedNodeId(node.id)"
> >
<template v-if="node.mixedContent && node.mixedContent.length > 0"> <template v-if="node.mixedContent && node.mixedContent.length > 0">
<span v-for="(mixed, idx) in node.mixedContent" :key="idx"> <span
v-for="(mixed, idx) in node.mixedContent"
:key="idx"
:data-node-id="mixed.type === 'text' ? `${node.id}-txt-${idx}` : undefined"
@click.stop="mixed.type === 'text' ? editorStore.setSelectedNodeId(`${node.id}-txt-${idx}`) : undefined"
>
<template v-if="mixed.type === 'text'">{{ mixed.text }}</template> <template v-if="mixed.type === 'text'">{{ mixed.text }}</template>
<template v-else-if="mixed.type === 'element'"> <template v-else-if="mixed.type === 'element'">
<DocNodeRenderer :node="node.children.find((c) => c.id === mixed.nodeId)!" :parent="node" is-inline /> <DocNodeRenderer :node="node.children.find((c) => c.id === mixed.nodeId)!" :parent="node" is-inline />
......
...@@ -346,15 +346,11 @@ export function useEditAreaContextMenu(props: EditAreaContextMenuProps, emit: (e ...@@ -346,15 +346,11 @@ export function useEditAreaContextMenu(props: EditAreaContextMenuProps, emit: (e
const mapped = editorStore.nodeMap.get(realId) const mapped = editorStore.nodeMap.get(realId)
if (!mapped) return if (!mapped) return
// 1. 编辑节点属性 // 1. 编辑节点 (包括元素属性与文本片段内容)
if (actionName === 'editNode') { if (actionName === 'editNode') {
if (isVirtual) { addNodeTargetId.value = nodeId
window.$message.warning('文本节点无属性可编辑,请直接在内容区双击编辑文本')
return
}
addNodeTargetId.value = realId
addNodeMode.value = 'edit' addNodeMode.value = 'edit'
addNodeAllowedTags.value = [mapped.node.tagName] addNodeAllowedTags.value = [isVirtual ? '#text' : mapped.node.tagName]
addNodeVisible.value = true addNodeVisible.value = true
} }
...@@ -370,25 +366,50 @@ export function useEditAreaContextMenu(props: EditAreaContextMenuProps, emit: (e ...@@ -370,25 +366,50 @@ export function useEditAreaContextMenu(props: EditAreaContextMenuProps, emit: (e
// 3. 删除节点 // 3. 删除节点
else if (actionName === 'deleteNode') { else if (actionName === 'deleteNode') {
if (isVirtual) {
window.$message.warning('文本节点暂不支持直接删除,请直接编辑清空内容')
return
}
try { try {
await window.$dialog.warning({ await window.$dialog.warning({
title: '确认删除', title: '确认删除',
content: `您确定要删除选中的节点 <${mapped.node.tagName}> 吗?该操作不可撤销。` content: isVirtual ? '您确定要删除此段文本内容吗?该操作不可撤销。' : `您确定要删除选中的节点 <${mapped?.node?.tagName || ''}> 吗?该操作不可撤销。`
}) })
editorStore.deleteNode(realId)
window.$message.success('节点删除成功')
if (editorStore.selectedNodeId && editorStore.nodeMap.has(editorStore.selectedNodeId)) { if (isVirtual) {
activeNodeId.value = editorStore.selectedNodeId const realParentId = activeNodeId.value.split('-txt-')[0]
const textIdx = parseInt(activeNodeId.value.split('-txt-')[1], 10)
const parentItem = editorStore.nodeMap.get(realParentId)
if (parentItem && Array.isArray(parentItem.node.mixedContent)) {
editorStore.saveSnapshot()
if (parentItem.node.mixedContent[textIdx]?.type === 'text') {
parentItem.node.mixedContent.splice(textIdx, 1)
} else {
const foundIdx = parentItem.node.mixedContent.findIndex((m: any) => m.type === 'text')
if (foundIdx !== -1) {
parentItem.node.mixedContent.splice(foundIdx, 1)
}
}
// 重新拼合父节点的 textContent
parentItem.node.textContent = parentItem.node.mixedContent
.map((item: any) => (item.type === 'text' ? item.text || '' : ''))
.join('')
editorStore.rebuildNodeMap()
editorStore.setSelectedNodeId(realParentId)
activeNodeId.value = realParentId
window.$message.success('已成功删除文本内容')
}
} else { } else {
emit('update:visible', false) // 若没有存活选中节点,则主动关闭当前弹窗 editorStore.deleteNode(realId)
window.$message.success('节点删除成功')
if (editorStore.selectedNodeId && editorStore.nodeMap.has(editorStore.selectedNodeId)) {
activeNodeId.value = editorStore.selectedNodeId
} else {
emit('update:visible', false) // 若没有存活选中节点,则主动关闭当前弹窗
}
} }
} catch (e) { } catch (e) {
// 取消 // 用户取消
} }
} }
...@@ -546,10 +567,20 @@ export function useEditAreaContextMenu(props: EditAreaContextMenuProps, emit: (e ...@@ -546,10 +567,20 @@ export function useEditAreaContextMenu(props: EditAreaContextMenuProps, emit: (e
return return
} }
editorStore.saveSnapshot() editorStore.saveSnapshot()
const oldMcIdx = isVirtual ? parseInt(nodeIdToMove.split('-txt-')[1], 10) : -1
const movedTextVal = isVirtual && parent.mixedContent?.[oldMcIdx] ? parent.mixedContent[oldMcIdx].text || '' : ''
const success = moveNodeInParent(parent, nodeIdToMove, 'up', isVirtual) const success = moveNodeInParent(parent, nodeIdToMove, 'up', isVirtual)
if (success) { if (success) {
editorStore.rebuildNodeMap() editorStore.rebuildNodeMap()
if (!isVirtual) { if (isVirtual) {
const newMcIdx = parent.mixedContent.findIndex((m) => m.type === 'text' && (m.text || '').trim() === movedTextVal.trim())
const targetIdx = newMcIdx !== -1 ? newMcIdx : Math.max(0, oldMcIdx - 1)
const newVirtualId = `${realId}-txt-${targetIdx}`
activeNodeId.value = newVirtualId
editorStore.setSelectedNodeId(newVirtualId)
} else {
editorStore.setSelectedNodeId(realId) editorStore.setSelectedNodeId(realId)
} }
window.$message.success('上移成功') window.$message.success('上移成功')
...@@ -570,10 +601,20 @@ export function useEditAreaContextMenu(props: EditAreaContextMenuProps, emit: (e ...@@ -570,10 +601,20 @@ export function useEditAreaContextMenu(props: EditAreaContextMenuProps, emit: (e
return return
} }
editorStore.saveSnapshot() editorStore.saveSnapshot()
const oldMcIdx = isVirtual ? parseInt(nodeIdToMove.split('-txt-')[1], 10) : -1
const movedTextVal = isVirtual && parent.mixedContent?.[oldMcIdx] ? parent.mixedContent[oldMcIdx].text || '' : ''
const success = moveNodeInParent(parent, nodeIdToMove, 'down', isVirtual) const success = moveNodeInParent(parent, nodeIdToMove, 'down', isVirtual)
if (success) { if (success) {
editorStore.rebuildNodeMap() editorStore.rebuildNodeMap()
if (!isVirtual) { if (isVirtual) {
const newMcIdx = parent.mixedContent.findIndex((m) => m.type === 'text' && (m.text || '').trim() === movedTextVal.trim())
const targetIdx = newMcIdx !== -1 ? newMcIdx : oldMcIdx + 1
const newVirtualId = `${realId}-txt-${targetIdx}`
activeNodeId.value = newVirtualId
editorStore.setSelectedNodeId(newVirtualId)
} else {
editorStore.setSelectedNodeId(realId) editorStore.setSelectedNodeId(realId)
} }
window.$message.success('下移成功') window.$message.success('下移成功')
...@@ -589,11 +630,14 @@ export function useEditAreaContextMenu(props: EditAreaContextMenuProps, emit: (e ...@@ -589,11 +630,14 @@ export function useEditAreaContextMenu(props: EditAreaContextMenuProps, emit: (e
{ label: '粘贴为子节点', key: 'insertFragmentInside', disabled: isTextNode.value || !allowedChildrenList.value.length } { label: '粘贴为子节点', key: 'insertFragmentInside', disabled: isTextNode.value || !allowedChildrenList.value.length }
]) ])
const editStructureOptions = computed(() => [ const editStructureOptions = computed(() => {
{ label: '添加子节点', key: 'addChildNode', disabled: isTextNode.value || !allowedChildrenList.value.length }, const disableChild = isTextNode.value ? !insertableParentList.value.length : !allowedChildrenList.value.length
{ label: '插入到上方', key: 'insertBefore', disabled: !insertableParentList.value.length }, return [
{ label: '插入到下方', key: 'insertAfter', disabled: !insertableParentList.value.length } { label: isTextNode.value ? '插入行内元素' : '添加子节点', key: 'addChildNode', disabled: disableChild },
]) { label: '插入到上方', key: 'insertBefore', disabled: !insertableParentList.value.length },
{ label: '插入到下方', key: 'insertAfter', disabled: !insertableParentList.value.length }
]
})
const editOrderOptions = computed(() => [ const editOrderOptions = computed(() => [
{ label: '上移', key: 'moveUp', disabled: !canMoveUpActive.value }, { label: '上移', key: 'moveUp', disabled: !canMoveUpActive.value },
......
...@@ -46,120 +46,126 @@ ...@@ -46,120 +46,126 @@
</div> </div>
<template v-else> <template v-else>
<div class="grid grid-cols-3 gap-2"> <div class="flex flex-col gap-2 pt-1">
<!-- 1. 编辑节点 --> <!-- 第一排:核心编辑与结构 (编辑节点 | 编辑结构 | 编辑顺序) -->
<CommonButton type="success" secondary block @click="runAction('editNode')"> <div class="grid grid-cols-3 gap-2">
<template #icon> <CommonButton type="primary" block @click="runAction('editNode')">
<n-icon><create-outline /></n-icon>
</template>
编辑节点
</CommonButton>
<!-- 2. 复制节点 -->
<CommonButton type="primary" secondary block @click="runAction('copyNode')">
<template #icon>
<n-icon><copy-outline /></n-icon>
</template>
复制节点
</CommonButton>
<!-- 3. 粘贴 XML -->
<n-dropdown trigger="click" :options="pasteXmlOptions" :disabled="!hasCopyCache" @select="runAction">
<CommonButton type="success" secondary block :disabled="!hasCopyCache">
<template #icon> <template #icon>
<n-icon><clipboard-outline /></n-icon> <n-icon><create-outline /></n-icon>
</template> </template>
粘贴XML 编辑节点
</CommonButton> </CommonButton>
</n-dropdown>
</div>
<div class="grid grid-cols-4 gap-2"> <n-dropdown
<!-- 4. 删除节点 --> trigger="click"
<CommonButton type="error" secondary block :disabled="!canDeleteActive" @click="runAction('deleteNode')"> :options="editStructureOptions"
<template #icon> :disabled="!editStructureOptions.some((o) => !o.disabled)"
<n-icon><trash-outline /></n-icon> @select="runAction"
</template> >
删除节点 <CommonButton secondary block :disabled="!editStructureOptions.some((o) => !o.disabled)">
</CommonButton> <template #icon>
<n-icon class="text-amber-500"><build-outline /></n-icon>
<!-- 5. 编辑结构 --> </template>
<n-dropdown trigger="click" :options="editStructureOptions" :disabled="isTextNode" @select="runAction"> 编辑结构
<CommonButton type="warning" secondary block :disabled="isTextNode"> </CommonButton>
</n-dropdown>
<n-dropdown
trigger="click"
:options="editOrderOptions"
:disabled="!canMoveUpActive && !canMoveDownActive"
@select="runAction"
>
<CommonButton secondary block :disabled="!canMoveUpActive && !canMoveDownActive">
<template #icon>
<n-icon class="text-amber-500"><swap-vertical-outline /></n-icon>
</template>
编辑顺序
</CommonButton>
</n-dropdown>
</div>
<!-- 第二排:剪贴板类 (复制节点 | 粘贴XML | 粘贴节点) -->
<div class="grid grid-cols-3 gap-2">
<CommonButton secondary block @click="runAction('copyNode')">
<template #icon> <template #icon>
<n-icon><build-outline /></n-icon> <n-icon class="text-primary"><copy-outline /></n-icon>
</template> </template>
编辑结构 复制节点
</CommonButton> </CommonButton>
</n-dropdown>
<n-dropdown trigger="click" :options="pasteXmlOptions" :disabled="!hasCopyCache" @select="runAction">
<!-- 5b. 编辑顺序 --> <CommonButton secondary block :disabled="!hasCopyCache">
<n-dropdown <template #icon>
trigger="click" <n-icon class="text-primary"><clipboard-outline /></n-icon>
:options="editOrderOptions" </template>
:disabled="isTextNode || (!canMoveUpActive && !canMoveDownActive)" 粘贴XML
@select="runAction" </CommonButton>
> </n-dropdown>
<CommonButton type="warning" secondary block :disabled="isTextNode || (!canMoveUpActive && !canMoveDownActive)">
<n-dropdown trigger="click" :options="pasteNodeOptions" :disabled="!hasCopyCache" @select="runAction">
<CommonButton secondary block :disabled="!hasCopyCache">
<template #icon>
<n-icon class="text-primary"><clipboard-outline /></n-icon>
</template>
粘贴节点
</CommonButton>
</n-dropdown>
</div>
<!-- 第三排:查看与检索类 (查看 XML | 查看规则 | 智能翻译) -->
<div class="grid grid-cols-3 gap-2">
<CommonButton secondary block @click="runAction('viewXml')">
<template #icon> <template #icon>
<n-icon><swap-vertical-outline /></n-icon> <n-icon class="text-sky-500"><code-working-outline /></n-icon>
</template> </template>
编辑顺序 查看 XML
</CommonButton> </CommonButton>
</n-dropdown>
<!-- 6. 粘贴节点 (DTD) --> <CommonButton secondary block :disabled="isTextNode" @click="runAction('checkRule')">
<n-dropdown trigger="click" :options="pasteNodeOptions" :disabled="!hasCopyCache" @select="runAction">
<CommonButton type="warning" secondary block :disabled="!hasCopyCache">
<template #icon> <template #icon>
<n-icon><clipboard-outline /></n-icon> <n-icon class="text-sky-500"><eye-outline /></n-icon>
</template> </template>
粘贴节点 查看规则
</CommonButton> </CommonButton>
</n-dropdown>
</div>
<div class="grid grid-cols-3 gap-2"> <CommonButton
<!-- 7. 智能翻译 --> secondary
<CommonButton block
type="info" :disabled="!canTranslateActive || isTranslating(activeNodeId)"
secondary @click="runAction('translateNode')"
block >
:disabled="!canTranslateActive || isTranslating(activeNodeId)" <template #icon>
@click="runAction('translateNode')" <n-icon v-if="isTranslating(activeNodeId)" class="animate-spin text-sky-500"><sync-outline /></n-icon>
> <n-icon v-else class="text-sky-500"><language-outline /></n-icon>
<template #icon> </template>
<n-icon v-if="isTranslating(activeNodeId)" class="animate-spin"><sync-outline /></n-icon> 智能翻译
<n-icon v-else><language-outline /></n-icon> </CommonButton>
</template> </div>
智能翻译
</CommonButton>
<!-- 8. 查看 XML 片段 -->
<CommonButton type="info" secondary block @click="runAction('viewXml')">
<template #icon>
<n-icon><code-working-outline /></n-icon>
</template>
查看 XML
</CommonButton>
<!-- 9. 查看 DTD 规则 -->
<CommonButton type="info" secondary block :disabled="isTextNode" @click="runAction('checkRule')">
<template #icon>
<n-icon><eye-outline /></n-icon>
</template>
查看规则
</CommonButton>
</div>
<!-- 保存为模板 --> <!-- 第四排:保存与删除类 (保存为模板 | 删除节点) -->
<div class="grid grid-cols-3 gap-2"> <div class="grid grid-cols-3 gap-2">
<CommonButton type="primary" secondary block class="col-span-3" @click="runAction('saveTemplate')"> <CommonButton secondary block class="col-span-2" @click="runAction('saveTemplate')">
<template #icon> <template #icon>
<n-icon><save-outline /></n-icon> <n-icon class="text-purple-500"><save-outline /></n-icon>
</template> </template>
保存为模板 保存为模板
</CommonButton> </CommonButton>
<CommonButton
type="error"
secondary
block
class="col-span-1"
:disabled="!canDeleteActive"
@click="runAction('deleteNode')"
>
<template #icon>
<n-icon><trash-outline /></n-icon>
</template>
删除节点
</CommonButton>
</div>
</div> </div>
</template> </template>
</div> </div>
......
...@@ -140,9 +140,14 @@ useKeyboardShortcuts({ ...@@ -140,9 +140,14 @@ useKeyboardShortcuts({
const handleEditSelectedNode = () => { const handleEditSelectedNode = () => {
if (!selectedNode.value) return if (!selectedNode.value) return
addNodeTargetId.value = selectedNode.value.id const targetId = editorStore.selectedNodeId || selectedNode.value.id
addNodeTargetId.value = targetId
addNodeMode.value = 'edit' addNodeMode.value = 'edit'
addNodeAllowedTags.value = [selectedNode.value.tagName] if (targetId.includes('-txt-')) {
addNodeAllowedTags.value = ['#text']
} else {
addNodeAllowedTags.value = [selectedNode.value.tagName]
}
addNodeVisible.value = true addNodeVisible.value = true
} }
...@@ -155,18 +160,31 @@ const handleContextMenu = (e: MouseEvent) => { ...@@ -155,18 +160,31 @@ const handleContextMenu = (e: MouseEvent) => {
const wrapper = target.closest('[data-node-id]') const wrapper = target.closest('[data-node-id]')
if (!wrapper) return if (!wrapper) return
const nodeId = wrapper.getAttribute('data-node-id') const rawNodeId = wrapper.getAttribute('data-node-id')
if (!nodeId) return if (!rawNodeId) return
const isVirtualText = rawNodeId.includes('-txt-')
const realId = isVirtualText ? rawNodeId.split('-txt-')[0] : rawNodeId
// 向上回溯构建祖先节点 ID 链 // 向上回溯构建祖先节点 ID 链
const ids: string[] = [] const ids: string[] = []
let currentId: string | null = nodeId if (isVirtualText) {
ids.push(rawNodeId)
}
let currentId: string | null = realId
let isInsideTable = false let isInsideTable = false
while (currentId) { while (currentId) {
const item = editorStore.nodeMap.get(currentId) const item = editorStore.nodeMap.get(currentId)
if (item) { if (item) {
if (item.node.tagName === 'TABLE' || item.node.tagName === 'ENTRY' || item.node.tagName === 'ROW') { if (
item.node.tagName === 'TABLE' ||
item.node.tagName === 'ENTRY' ||
item.node.tagName === 'ROW' ||
item.node.tagName === 'TBODY' ||
item.node.tagName === 'TGROUP'
) {
isInsideTable = true isInsideTable = true
} }
ids.push(currentId) ids.push(currentId)
......
...@@ -137,7 +137,12 @@ export function useAddNodeModal(formRef: Ref<FormInst | null>) { ...@@ -137,7 +137,12 @@ export function useAddNodeModal(formRef: Ref<FormInst | null>) {
if (node) { if (node) {
form.tagName = node.tagName form.tagName = node.tagName
form.textContent = node.textContent || '' if (isMixedContentElement(node.tagName) && Array.isArray(node.mixedContent) && node.mixedContent.length > 0) {
const firstText = node.mixedContent.find((i) => i.type === 'text')?.text
form.textContent = firstText !== undefined && firstText !== '' ? firstText : (node.textContent || '')
} else {
form.textContent = node.textContent || ''
}
const defs = getElementAttributes(node.tagName) const defs = getElementAttributes(node.tagName)
const attrs: Record<string, string | null> = {} const attrs: Record<string, string | null> = {}
for (const name of Object.keys(defs)) { for (const name of Object.keys(defs)) {
......
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