优化BlockOutlineRail.vue
This commit is contained in:
parent
7eaeda0bfe
commit
c8b32273e4
@ -10,6 +10,7 @@
|
||||
*/
|
||||
import { ref, computed, onMounted, onBeforeUnmount, nextTick } from 'vue'
|
||||
import type { Editor } from '@tiptap/core'
|
||||
import { TextSelection } from '@tiptap/pm/state'
|
||||
|
||||
interface BlockInfo {
|
||||
pos: number
|
||||
@ -184,15 +185,88 @@ function onPopupLeave() {
|
||||
}
|
||||
|
||||
function scrollToBlock(block: BlockInfo) {
|
||||
try {
|
||||
const domNode = props.editor.view.nodeDOM(block.pos)
|
||||
if (domNode instanceof HTMLElement) {
|
||||
domNode.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
props.editor.commands.setTextSelection(block.pos)
|
||||
activeBlockPos.value = block.pos
|
||||
|
||||
const { view } = props.editor
|
||||
|
||||
console.group('[BlockOutlineRail] scrollToBlock')
|
||||
console.log('block:', { pos: block.pos, text: block.text, level: block.level })
|
||||
|
||||
// ProseMirror transaction: 设置选区到 heading 行内内容
|
||||
view.dispatch(
|
||||
view.state.tr.setSelection(
|
||||
TextSelection.create(view.state.doc, block.pos + 1),
|
||||
),
|
||||
)
|
||||
|
||||
// 定位标题到编辑器内容区最上方
|
||||
const domNode = view.nodeDOM(block.pos)
|
||||
console.log('domNode:', domNode)
|
||||
if (domNode instanceof HTMLElement) {
|
||||
const container = findScrollContainer(view.dom as HTMLElement)
|
||||
const containerRect = container instanceof HTMLElement
|
||||
? container.getBoundingClientRect()
|
||||
: new DOMRect(0, 0, window.innerWidth, window.innerHeight)
|
||||
const headingRect = domNode.getBoundingClientRect()
|
||||
const offset = headingRect.top - containerRect.top
|
||||
console.log('container:', container === window ? 'window' : (container as HTMLElement).className || (container as HTMLElement).tagName)
|
||||
console.log('containerRect.top:', containerRect.top)
|
||||
console.log('headingRect.top:', headingRect.top)
|
||||
console.log('offset:', offset)
|
||||
if (Math.abs(offset) > 2) {
|
||||
console.log('>>> scrolling by', offset)
|
||||
if (container === window) {
|
||||
window.scrollBy({ top: offset })
|
||||
} else {
|
||||
const el = container as HTMLElement
|
||||
console.log('current scrollTop:', el.scrollTop, '->', el.scrollTop + offset)
|
||||
el.scrollTop += offset
|
||||
console.log('after scrollTop:', el.scrollTop)
|
||||
}
|
||||
} else {
|
||||
console.log('>>> offset too small, skip scroll')
|
||||
}
|
||||
} catch {
|
||||
props.editor.commands.setTextSelection(block.pos)
|
||||
} else {
|
||||
console.warn('domNode is not HTMLElement:', typeof domNode, domNode)
|
||||
}
|
||||
|
||||
// 聚焦编辑器(阻止浏览器自动滚动,因为我们已精确控制了滚动位置)
|
||||
view.dom.focus({ preventScroll: true } as FocusOptions)
|
||||
|
||||
// 临时高亮
|
||||
if (domNode instanceof HTMLElement) {
|
||||
domNode.classList.add('block-outline-highlight')
|
||||
setTimeout(() => domNode.classList.remove('block-outline-highlight'), 2000)
|
||||
}
|
||||
console.groupEnd()
|
||||
}
|
||||
|
||||
/** 向上查找真正可滚动的祖先容器 */
|
||||
function findScrollContainer(el: HTMLElement): HTMLElement | Window {
|
||||
let parent: HTMLElement | null = el.parentElement
|
||||
while (parent) {
|
||||
if (parent === document.body) {
|
||||
console.log('findScrollContainer: reached body, return window')
|
||||
return window
|
||||
}
|
||||
const style = window.getComputedStyle(parent)
|
||||
const isScrollable =
|
||||
style.overflow === 'auto' || style.overflow === 'scroll' ||
|
||||
style.overflowY === 'auto' || style.overflowY === 'scroll'
|
||||
const canScroll = isScrollable && parent.scrollHeight > parent.clientHeight
|
||||
console.log(
|
||||
'findScrollContainer:', (parent as HTMLElement).className || parent.tagName,
|
||||
'overflow:', style.overflowY,
|
||||
'scrollH:', parent.scrollHeight, 'clientH:', parent.clientHeight,
|
||||
'canScroll:', canScroll,
|
||||
)
|
||||
if (canScroll) {
|
||||
return parent
|
||||
}
|
||||
parent = parent.parentElement
|
||||
}
|
||||
console.log('findScrollContainer: no container found, return window')
|
||||
return window
|
||||
}
|
||||
|
||||
function getIcon(level?: number): string {
|
||||
@ -449,4 +523,18 @@ onBeforeUnmount(() => {
|
||||
color: #1e40af;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ── 点击跳转后编辑器内临时高亮 ── */
|
||||
@keyframes outline-highlight-pulse {
|
||||
0% { background-color: transparent; }
|
||||
15% { background-color: rgba(59, 130, 246, 0.18); }
|
||||
40% { background-color: rgba(59, 130, 246, 0.08); }
|
||||
80% { background-color: rgba(59, 130, 246, 0.03); }
|
||||
100% { background-color: transparent; }
|
||||
}
|
||||
|
||||
.block-outline-highlight {
|
||||
animation: outline-highlight-pulse 2s ease-out forwards;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user