Files
mathew/frontend/app/composables/useArticleTabs.ts
2026-09-30 01:49:35 +02:00

110 lines
4.3 KiB
TypeScript

export interface ArticleTab {
slug: string
title: string
/** the language version this page holds */
lang?: string
/** the article this document is a version of — its canonical slug */
identity?: string
}
const MAX_TABS = 10
/** The address a page of the book was read from: the article's own URL for
* the canonical version, /wiki/<lang>/<slug> for the others. */
export function tabHref(tab: Pick<ArticleTab, 'slug' | 'lang' | 'identity'>): string {
const identity = tab.identity ?? tab.slug
return versionHref(identity, { language: tab.lang, slug: tab.slug })
}
/** Which page of the stack an address points at, if any. */
export function tabAt(tabs: readonly ArticleTab[], path: string): ArticleTab | null {
const loc = parseReaderPath(path)
if (!loc) return null
const exact = tabs.find((t) => {
const identity = t.identity ?? t.slug
if (loc.lang) return identity === loc.identity && (t.lang ?? 'en') === loc.lang
return identity === loc.identity && t.slug === loc.identity
})
/* an address naming a document directly (older /slug-fr form) still finds it */
return exact ?? tabs.find((t) => t.slug === loc.identity) ?? null
}
/**
* The session stack of articles opened in the reader — the pages of the
* book, in the order they were opened, newest at the right. The page
* currently in view is simply wherever the route points; the stack lives in
* shared state and survives every client navigation (reset on reload), so the
* open pages are still there when you come back to the reading surface.
*/
export function useArticleTabs() {
const route = useRoute()
const router = useRouter()
const tabs = useState<ArticleTab[]>('article-tabs', () => [])
/* the last page seen outside the reading surface, so closing the last tab
returns there instead of always going home */
const origin = useState<string>('article-tabs:origin', () => '')
/* the page last in view, so the stack can be found again from any page that
hides the reading surface — the editor, a new article, the graph */
const lastSlug = useState<string>('article-tabs:last', () => '')
function ensureTab(
slug: string,
title: string,
meta?: { lang?: string; identity?: string }
) {
const existing = tabs.value.find((t) => t.slug === slug)
if (existing) {
existing.title = title // follow the tab label after a rename
if (meta?.lang) existing.lang = meta.lang
if (meta?.identity) existing.identity = meta.identity
return
}
const stack = [...tabs.value, { slug, title, lang: meta?.lang, identity: meta?.identity }]
if (stack.length > MAX_TABS) {
/* don't stack forever — retire the oldest tab that isn't in view */
const drop = stack.findIndex((t) => t.slug !== slug)
if (drop !== -1) stack.splice(drop, 1)
}
tabs.value = stack
}
/* Close a page. Closing one that isn't in view just takes it out of the
stack and the rest settle; closing the page in view turns back to the
page before it — or returns to the last page outside the book once the
last page is gone. */
function closeTab(slug: string) {
const at = tabs.value.findIndex((t) => t.slug === slug)
const isActive = tabAt(tabs.value, route.path)?.slug === slug
tabs.value = tabs.value.filter((t) => t.slug !== slug)
if (isActive) {
const back = tabs.value[Math.max(0, Math.min(at - 1, tabs.value.length - 1))]
void router.push(back ? tabHref(back) : origin.value || '/')
}
}
return { tabs, origin, lastSlug, ensureTab, closeTab }
}
/**
* Keeps the book's stack of pages in step with the route: which page was last
* in view, and the last plain page outside the reading surface. The stack
* itself is left alone — it survives the whole session, so wandering off to
* the graph or the article list and coming back finds the pages as they were.
* Call once, high in the app.
*/
export function useArticleTabsSync() {
const route = useRoute()
const { tabs, lastSlug, origin } = useArticleTabs()
watch(
() => route.fullPath,
(to) => {
/* filed under the tab's own slug, so a language version is found again
as the version it was, not merely as the article */
if (isReaderPath(to)) lastSlug.value = tabAt(tabs.value, to)?.slug ?? wikiSlugFromPath(to) ?? ''
else if (!isReadingSurface(to)) origin.value = to
},
{ immediate: true }
)
}