// The browser tab title, including WHICH record is open. // // The router can only name the KIND of page, because it runs before the view // has fetched anything: every application read " ShopDB - // Application", so two tabs on two different machines were indistinguishable, // and a shared link previewed as the section rather than the thing. // // So a detail view names itself once its record arrives: // // setRecordTitle(machine.assetnumber) // // THE ORDERING IS THE WHOLE PROBLEM. The router's afterEach awaits the site // settings before it can build " ShopDB - ", so on a cold load // it finishes AFTER a view that fetched quickly has already set a good title, // and would overwrite it with the generic one. Both writers therefore go // through here, and the generic write is dropped when a record title already // belongs to the route being landed on. The key is the route's fullPath, so // navigating away invalidates the previous record's claim. import { getFacilityName } from '@/utils/siteSettings' let activeKey = null // fullPath of the route the tab is currently showing let recordTitle = null // record name set by a view for activeKey, if any let sitePrefix = 'ShopDB' // ' ShopDB', resolved once and cached function compose(page) { return page && page !== 'Home' ? `${sitePrefix} - ${page}` : sitePrefix } async function resolveSitePrefix() { try { const facility = await getFacilityName() if (facility && facility !== 'ShopDB') sitePrefix = `${facility} ShopDB` } catch (settingsError) { // settings unavailable: plain ShopDB is a fine tab title } return sitePrefix } // Called by the router on every navigation, BEFORE it awaits anything, so a // view that resolves first is not treated as belonging to the previous route. export function beginNavigation(key) { activeKey = key recordTitle = null } // Called by the router once it knows the site prefix. Skipped when a view has // already named the record for this route. export async function applyGenericTitle(key, page) { await resolveSitePrefix() if (key !== activeKey) return // navigated on while we awaited // A view that named the record before the facility name arrived composed its // title against the placeholder prefix, so rewrite it rather than leaving // 'ShopDB - CSF16' without the facility name in front of it. document.title = compose(recordTitle !== null ? recordTitle : page) } // A tab shows very little, and the browser truncates mid-word wherever it runs // out. Cut at a word boundary instead, so what survives is readable. Knowledge // base articles are the case that forces this: their name is a whole sentence // of description, not a short label. const MAXLENGTH = 60 function shorten(text) { if (text.length <= MAXLENGTH) return text const cut = text.slice(0, MAXLENGTH) const space = cut.lastIndexOf(' ') return `${(space > MAXLENGTH / 2 ? cut.slice(0, space) : cut).trimEnd()}...` } // Called by a detail view once its record has loaded. An empty or missing name // leaves the generic title in place rather than showing a bare id: "Machine" // beats "Machine 4192" for somebody scanning a row of tabs, and a record that // failed to load has no name to show anyway. export function setRecordTitle(name) { const text = (name === null || name === undefined) ? '' : String(name).trim() if (!text) return recordTitle = shorten(text) document.title = compose(recordTitle) } // Exposed for tests. export function _state() { return { activeKey, recordTitle, sitePrefix } } export function _reset() { activeKey = null recordTitle = null sitePrefix = 'ShopDB' }