Files
mathew/frontend/app/composables/useArticleTabs.ts
Aran Roig 68b74eb60b
All checks were successful
Build and Deploy Nuxt / build (push) Successful in 24s
Graph calculator improvmentes
2026-10-02 14:53:00 +02:00

197 lines
7.9 KiB
TypeScript

import type { ToolPage } from '~/utils/routes'
export interface ArticleTab {
/** the article this page is of — its canonical slug, and the page's key */
identity: string
/** set when the page is one of the wiki's own pages — the link map, the
* graphing calculator — laid on the book instead of an article: the page
* draws that live surface, at its own address, for as long as it is open */
tool?: ToolPage
/** the language this page is turned to */
lang: string
/** the version in view, as the library files it: its stored slug */
slug: string
/** and the document behind it — which a retitle keeps, so the article's page
* follows it to its new address instead of a second page opening beside it */
docId?: string
title: string
}
const MAX_TABS = 10
/** What each of the wiki's own pages is called, as a message key — the label
* the book's spine, chips and dock wear for one of these pages. */
export const TOOL_LABEL_KEYS: Record<ToolPage, string> = {
graph: 'sidebar.graph',
calculator: 'sidebar.calculator',
}
/** The key of the book page holding one of the wiki's own pages. No article
* slug carries a colon, so this can never read as an article's identity. */
export function toolPageIdentity(page: ToolPage): string {
return `page:${page}`
}
/** The address a page of the book was read from: the version it is turned to,
* or the wiki's own page's address where the page holds one of those. */
export function tabHref(tab: Pick<ArticleTab, 'identity' | 'lang' | 'tool'>): string {
return tab.tool ? toolHref(tab.tool) : versionHref(tab.identity, tab.lang)
}
/** Which page of the stack an address points at, if any. An address that
* names no language points at the article's page however it is turned; the
* wiki's own pages are found by their own addresses. */
export function tabAt(tabs: readonly ArticleTab[], path: string): ArticleTab | null {
const page = toolFromPath(path)
if (page) return tabs.find((t) => t.tool === page) ?? null
const loc = parseReaderPath(path)
if (!loc) return null
/* one article is one page of the book, in whichever language that page is
turned to — so the address names its page by the article alone */
return tabs.find((t) => t.identity === 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. One article is one
* page however many languages it exists in, so turning an article to another
* language turns that page over instead of adding another to the row. 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 article 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 lastIdentity = useState<string>('article-tabs:last', () => '')
/**
* Lay one version on the book: a fresh page at the right of the row, or —
* when the article already has a page open — that same page turned to this
* language. Reading an article in French is not opening a second article,
* so the page keeps its place in the row and only its text changes.
*/
function openVersion(article: {
_id?: string
slug: string
title: string
language?: string
baseSlug?: string | null
}) {
const identity = identityOf(article)
const lang = articleLanguage(article)
/* the article's own page, or — failing that — the page holding this very
document, which is how a retitle is recognised: the article moved to a
new address and its page follows it rather than doubling up */
const page = tabs.value.find(
(t) => t.identity === identity || (!!article._id && t.docId === article._id)
)
if (page) {
/* the page follows the version it holds: the article's identity, the
document its language asks for, and the title that version carries (a
save that retitled an article moves it and relabels the page; a
language turn relabels it again and takes it to another document) */
page.identity = identity
page.docId = article._id
page.slug = article.slug
page.lang = lang
page.title = article.title
return
}
const stack = [
...tabs.value,
{
identity,
lang,
slug: article.slug,
docId: article._id,
title: article.title,
},
]
if (stack.length > MAX_TABS) {
/* don't stack forever — retire the oldest page that isn't this one */
const drop = stack.findIndex((t) => t.identity !== identity)
if (drop !== -1) stack.splice(drop, 1)
}
tabs.value = stack
}
/**
* Lay one of the wiki's own pages — the link map, the graphing calculator —
* on the book beside the articles. One surface is one page however many
* times it is asked for: asking again just finds the page already there.
*/
function openTool(page: ToolPage) {
const identity = toolPageIdentity(page)
if (tabs.value.some((t) => t.tool === page)) return
const stack = [
...tabs.value,
{ identity, tool: page, lang: DEFAULT_LANGUAGE, slug: identity, title: page },
]
if (stack.length > MAX_TABS) {
/* don't stack forever — retire the oldest page that isn't this one */
const drop = stack.findIndex((t) => t.identity !== identity)
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(identity: string) {
const at = tabs.value.findIndex((t) => t.identity === identity)
const isActive = tabAt(tabs.value, route.path)?.identity === identity
tabs.value = tabs.value.filter((t) => t.identity !== identity)
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, lastIdentity, openVersion, openTool, 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, lastIdentity, origin } = useArticleTabs()
watch(
() => route.fullPath,
(to) => {
/* filed under the article's identity, so a version turned to another
language is still found as the same page of the book */
if (isReaderPath(to)) {
lastIdentity.value = tabAt(tabs.value, to)?.identity ?? wikiSlugFromPath(to) ?? ''
return
}
/* one of the wiki's own pages held by the book belongs to the reading
surface too — it's where its page is in view, not a place to return
to; asked plain (no page open), it's an ordinary page of the site */
if (isToolPath(to)) {
const page = tabAt(tabs.value, to)
if (page) {
lastIdentity.value = page.identity
return
}
}
if (!isReadingSurface(to)) origin.value = to
},
{ immediate: true }
)
}