142 lines
5.7 KiB
TypeScript
142 lines
5.7 KiB
TypeScript
export interface ArticleTab {
|
|
/** the article this page is of — its canonical slug, and the page's key */
|
|
identity: string
|
|
/** 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
|
|
|
|
/** The address a page of the book was read from: the version it is turned to. */
|
|
export function tabHref(tab: Pick<ArticleTab, 'identity' | 'lang'>): string {
|
|
return 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. */
|
|
export function tabAt(tabs: readonly ArticleTab[], path: string): ArticleTab | 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
|
|
}
|
|
|
|
/* 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, 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) ?? ''
|
|
else if (!isReadingSurface(to)) origin.value = to
|
|
},
|
|
{ immediate: true }
|
|
)
|
|
}
|