Files
RawMark/web/src/app/TabBar.vue
T
eezd 64915a7b94 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
2026-10-17 17:44:05 +08:00

112 lines
3.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { ref } from 'vue'
import Icon from './Icon.vue'
import { noteTitle } from './paths'
import { closeTab, ws } from './workspace'
// 拖动排序:拖到某个标签的左半边插到它前面,右半边插到它后面
const TAB_TYPE = 'application/x-rawmark-tab'
const dragging = ref<string | null>(null)
const dropAt = ref<{ path: string; after: boolean } | null>(null)
function onDragStart(e: DragEvent, path: string) {
dragging.value = path
e.dataTransfer!.setData(TAB_TYPE, path)
e.dataTransfer!.effectAllowed = 'move'
}
function onDragOver(e: DragEvent, path: string) {
if (!e.dataTransfer?.types.includes(TAB_TYPE)) return
e.preventDefault()
const r = (e.currentTarget as HTMLElement).getBoundingClientRect()
dropAt.value = { path, after: e.clientX > r.left + r.width / 2 }
}
function onDrop(e: DragEvent) {
const from = dragging.value
const target = dropAt.value
dragging.value = dropAt.value = null
if (!from || !target || from === target.path) return
e.preventDefault()
const tab = ws.tabs.splice(
ws.tabs.findIndex((t) => t.path === from),
1,
)[0]
const i = ws.tabs.findIndex((t) => t.path === target.path) + (target.after ? 1 : 0)
ws.tabs.splice(i, 0, tab)
}
function onDragEnd() {
dragging.value = dropAt.value = null
}
// ---- 右键菜单 ----
const menu = ref<{ path: string; x: number; y: number } | null>(null)
function openMenu(e: MouseEvent, path: string) {
menu.value = { path, x: Math.min(e.clientX, innerWidth - 200), y: e.clientY }
}
async function closeMany(keep: (index: number, path: string) => boolean) {
const paths = ws.tabs.filter((t, i) => !keep(i, t.path)).map((t) => t.path)
menu.value = null
for (const p of paths) await closeTab(p)
}
function closeOthers(path: string) {
void closeMany((_, p) => p === path)
}
function closeRight(path: string) {
const at = ws.tabs.findIndex((t) => t.path === path)
void closeMany((i) => i <= at)
}
</script>
<template>
<nav class="rm-tabs" :class="{ empty: !ws.tabs.length && !ws.sidebarCollapsed }" role="tablist">
<button v-if="ws.sidebarCollapsed" class="rm-icon-btn rm-tabs-side" title="显示侧栏(Ctrl+\)" @click="ws.sidebarCollapsed = false">
<Icon name="menu" />
</button>
<div
v-for="tab in ws.tabs"
:key="tab.path"
class="rm-tab"
:class="{
active: ws.active === tab.path,
dragging: dragging === tab.path,
'drop-before': dropAt?.path === tab.path && !dropAt.after,
'drop-after': dropAt?.path === tab.path && dropAt.after,
}"
role="tab"
:aria-selected="ws.active === tab.path"
:title="tab.path"
draggable="true"
@click="ws.active = tab.path"
@auxclick.prevent="$event.button === 1 && closeTab(tab.path)"
@contextmenu.prevent="openMenu($event, tab.path)"
@dragstart="onDragStart($event, tab.path)"
@dragover="onDragOver($event, tab.path)"
@drop="onDrop"
@dragend="onDragEnd"
>
<span class="rm-tab-title">{{ noteTitle(tab.path) }}</span>
<button class="rm-tab-close" :class="{ dirty: tab.dirty }" title="关闭" @click.stop="closeTab(tab.path)">
<span class="rm-dirty-dot" />
<Icon name="close" :size="12" />
</button>
</div>
<button v-if="ws.active" class="rm-icon-btn rm-tabs-outline" :class="{ on: ws.outlineOpen }" title="大纲(Ctrl+Shift+O)" @click="ws.outlineOpen = !ws.outlineOpen">
<Icon name="outline" />
</button>
<template v-if="menu">
<div class="rm-menu-backdrop" @click="menu = null" @contextmenu.prevent="menu = null" />
<div class="rm-menu rm-tab-menu" :style="{ left: menu.x + 'px', top: menu.y + 'px' }">
<button @click="closeMany((_, p) => p !== menu!.path)">关闭</button>
<button @click="closeOthers(menu.path)">关闭其他标签页</button>
<button @click="closeRight(menu.path)">关闭右侧标签页</button>
</div>
</template>
</nav>
</template>