Add tab reordering, sidebar resize and collapse, and outline panel
- Drag tabs to reorder; context menu to close, close others, close right - Resizable sidebar width (synced setting), collapsible with Mod+\ - Outline panel (Mod+Shift+O): right column on desktop, overlay on mobile - Mobile: title opens quick switcher, swipe left closes the drawer, 44px touch targets
This commit is contained in:
1 parent
91da6e116e
commit
64915a7b94
7 files changed
+322
-15
No files matched your search
@@ -0,0 +1,107 @@
|
||||
<script setup lang="ts">
|
||||
import { ensureSyntaxTree, syntaxTree } from '@codemirror/language'
|
||||
import type { EditorState } from '@codemirror/state'
|
||||
import { EditorView } from '@codemirror/view'
|
||||
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import Icon from './Icon.vue'
|
||||
import { activeTab, editorView, onEditorUpdate, ws } from './workspace'
|
||||
|
||||
// 大纲:当前笔记的标题列表,点击跳转;高亮光标所在的章节
|
||||
interface Heading {
|
||||
level: number
|
||||
text: string
|
||||
from: number
|
||||
}
|
||||
|
||||
const headings = ref<Heading[]>([])
|
||||
const current = ref(-1)
|
||||
|
||||
function collect(state: EditorState): Heading[] {
|
||||
// 长文档的语法树可能尚未解析完,最多等 50ms,之后随编辑更新
|
||||
const tree = ensureSyntaxTree(state, state.doc.length, 50) ?? syntaxTree(state)
|
||||
const out: Heading[] = []
|
||||
tree.iterate({
|
||||
enter(n) {
|
||||
const m = /^(ATX|Setext)Heading(\d)$/.exec(n.name)
|
||||
if (!m) return
|
||||
const line = state.doc.lineAt(n.from)
|
||||
const text = m[1] === 'ATX' ? line.text.replace(/^\s*#{1,6}\s*/, '').replace(/\s+#+\s*$/, '') : line.text.trim()
|
||||
out.push({ level: Number(m[2]), text: text || '(无标题)', from: n.from })
|
||||
return false
|
||||
},
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
let timer: ReturnType<typeof setTimeout> | undefined
|
||||
let lastDoc: unknown
|
||||
function refresh() {
|
||||
const v = editorView()
|
||||
if (!v || activeTab()?.status !== 'ready') {
|
||||
headings.value = []
|
||||
lastDoc = undefined
|
||||
return
|
||||
}
|
||||
// 只移动光标时不重新收集标题
|
||||
if (v.state.doc !== lastDoc) {
|
||||
headings.value = collect(v.state)
|
||||
lastDoc = v.state.doc
|
||||
}
|
||||
const head = v.state.selection.main.head
|
||||
let i = headings.value.length - 1
|
||||
while (i >= 0 && headings.value[i].from > head) i--
|
||||
current.value = i
|
||||
}
|
||||
function scheduleRefresh() {
|
||||
clearTimeout(timer)
|
||||
timer = setTimeout(refresh, 150)
|
||||
}
|
||||
|
||||
function jump(h: Heading) {
|
||||
const v = editorView()
|
||||
if (!v) return
|
||||
v.dispatch({ selection: { anchor: h.from }, effects: EditorView.scrollIntoView(h.from, { y: 'start', yMargin: 16 }) })
|
||||
if (matchMedia('(max-width: 768px)').matches) ws.outlineOpen = false
|
||||
else v.focus()
|
||||
}
|
||||
|
||||
let unsubscribe: (() => void) | undefined
|
||||
onMounted(() => {
|
||||
unsubscribe = onEditorUpdate(scheduleRefresh)
|
||||
// 切换标签页时编辑器整体替换状态,不经过更新监听;等编辑器面板换好状态后再读
|
||||
requestAnimationFrame(refresh)
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
unsubscribe?.()
|
||||
clearTimeout(timer)
|
||||
})
|
||||
watch(
|
||||
() => [ws.active, activeTab()?.status],
|
||||
() => requestAnimationFrame(refresh),
|
||||
)
|
||||
const minLevel = () => Math.min(...headings.value.map((h) => h.level))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="rm-outline-backdrop" @click.self="ws.outlineOpen = false">
|
||||
<aside class="rm-outline" aria-label="大纲">
|
||||
<div class="rm-outline-head">
|
||||
<span>大纲</span>
|
||||
<button class="rm-icon-btn" title="关闭大纲" @click="ws.outlineOpen = false"><Icon name="close" /></button>
|
||||
</div>
|
||||
<ul v-if="headings.length" class="rm-outline-list">
|
||||
<li
|
||||
v-for="(h, i) in headings"
|
||||
:key="h.from"
|
||||
:class="{ on: i === current }"
|
||||
:style="{ paddingLeft: (h.level - minLevel()) * 14 + 12 + 'px' }"
|
||||
@mousedown.prevent
|
||||
@click="jump(h)"
|
||||
>
|
||||
{{ h.text }}
|
||||
</li>
|
||||
</ul>
|
||||
<p v-else class="rm-tree-empty">没有标题</p>
|
||||
</aside>
|
||||
</div>
|
||||
</template>
|
||||
Reference in new issue
Block a user