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 = { 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): 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('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('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('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 } ) }