Compare commits
2 commits
main
...
regions-pl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
728970b2bf | ||
|
|
00f7d349eb |
2 changed files with 120 additions and 23 deletions
|
|
@ -22,6 +22,9 @@
|
|||
// reader's scroll settles is a replace; Back/Forward scroll and never write; nested it announces; without the
|
||||
// opt-in it never writes; a kept-alive column shown again claims its pane back (a replace), unless the address
|
||||
// names one.
|
||||
// H. NESTED SHELLS at any depth: a nested shell's tab is recorded as segment `depth` of ?subtab= (never as the
|
||||
// owner's tab); a hole is refused; the same tab again is a no-op; a segment change drops the deeper segments and
|
||||
// pane/place; depth 1, 2 and 3; Back steps a segment; the owner's tab change drops every segment.
|
||||
// G. THE WIRING in Regions: regions-mfe opts its column in, owns its leaf, and carries the place with its pane;
|
||||
// region-dashboards claims ?place= (replace at first paint, push on a choice) and re-applies it on Back. And
|
||||
// each shell (Plugin Studio, the Research Gallery, Posterverse) records its nested composites' choices
|
||||
|
|
@ -115,7 +118,13 @@ async function arms({ va, sv, counts, pw, src }, { skipF = false } = {}) {
|
|||
for (const bad of ['a'.repeat(65), '<b>', 'a b', '-lead', '../x', 'x?y', 'x#y', '']) {
|
||||
if (va.cleanKey(bad) !== '') fails.push(`A: the whitelist lets ${JSON.stringify(bad.slice(0, 20))} through`)
|
||||
}
|
||||
eq('A: an unknown or malformed value reads as absent', va.readViewKeys('?tab=a%20b&pane=%E0%A4%A&place=angola&tab=second'), { tab: '', pane: '', place: 'angola' })
|
||||
eq('A: an unknown or malformed value reads as absent', va.readViewKeys('?tab=a%20b&pane=%E0%A4%A&place=angola&tab=second'), { tab: '', subtab: '', pane: '', place: 'angola' })
|
||||
// the subtab PATH: plain-id segments joined by '.', at most 6; anything else reads as absent
|
||||
eq('A: a subtab path reads segment by segment', ['?subtab=a', '?subtab=a.b-c.d_e', '?subtab=a..b', '?subtab=.a', '?subtab=a.', '?subtab=a.b%20c', '?subtab=1.2.3.4.5.6', '?subtab=1.2.3.4.5.6.7', '?subtab=-a'].map((q) => va.readViewKeys(q).subtab), ['a', 'a.b-c.d_e', '', '', '', '', '1.2.3.4.5.6', '', ''])
|
||||
eq('A: subtabAt reads one depth', [va.subtabAt('?subtab=a.b.c', 1), va.subtabAt('?subtab=a.b.c', 3), va.subtabAt('?subtab=a.b.c', 4), va.subtabAt('', 1)], ['a', 'c', '', ''])
|
||||
eq('A: depthOf', [{ addressDepth: 3 }, { addressDepth: '2' }, { addressDepth: 99 }, { addressDepth: 0 }, {}, null].map(va.depthOf), [3, 2, 6, 1, 1, 1])
|
||||
eq('A: a tab change drops subtab, pane and place', va.viewSearch('?tab=a&subtab=x.y&pane=p&place=c', { tab: 'b' }), '?tab=b')
|
||||
eq('A: a subtab change drops pane and place, keeps tab', va.viewSearch('?tab=a&subtab=x&pane=p&place=c', { subtab: 'z' }), '?tab=a&subtab=z')
|
||||
eq('A: a tab change drops pane and place', va.viewSearch('?tab=a&pane=b&place=c&lang=ar', { tab: 'd' }), '?tab=d&lang=ar')
|
||||
eq('A: a pane change keeps tab and place (siblings)', va.viewSearch('?tab=a&pane=b&place=c', { pane: 'e' }), '?tab=a&pane=e&place=c')
|
||||
eq('A: a place change keeps tab and pane', va.viewSearch('?tab=a&pane=b&place=c', { place: 'f' }), '?tab=a&pane=b&place=f')
|
||||
|
|
@ -246,15 +255,61 @@ async function arms({ va, sv, counts, pw, src }, { skipF = false } = {}) {
|
|||
if (b.writes.push !== 1 || b.writes.replace) fails.push('E: ownAddress wrote while the page has no owner')
|
||||
if (e2.cancelBubble) fails.push('E: an announcement an element does not own was stopped (the real owner further out never hears it)')
|
||||
owner = 'app'
|
||||
const before = b.at()
|
||||
fire({ tab: 'posters' })
|
||||
if (b.at() !== before) fails.push(`E: a nested announcement switched the OWNER's tab: ${b.at()}`)
|
||||
off(); fire({ place: 'benin' })
|
||||
if (b.writes.push !== 1) fails.push('E: ownAddress off() left its listener')
|
||||
b.hist.back()
|
||||
if (b.at() !== '/shell?tab=regions&lang=ar') fails.push(`E: Back after a pane + place click did not return in one step: ${b.at()}`)
|
||||
}
|
||||
|
||||
// ── H. NESTED SHELLS at any depth: Plugin Studio owns the address; shells nested at depth 1, 2, 3 claim their tabs ──
|
||||
{
|
||||
const b = browser('https://x.test/?tab=doxa-maps-spa__doxa-maps-spa')
|
||||
const el = new EventTarget()
|
||||
const off = va.ownAddress(el, () => 'app')
|
||||
const shell = (depth, tab, push = true) => { const e = new CustomEvent(va.VIEW_EVENT, { bubbles: true, composed: true, detail: { changes: { tab }, push, depth } }); el.dispatchEvent(e); return e }
|
||||
const comp = (changes) => el.dispatchEvent(new CustomEvent(va.VIEW_EVENT, { bubbles: true, composed: true, detail: { changes, push: true } }))
|
||||
const q = () => b.q()
|
||||
shell(2, 'upg-100', false)
|
||||
if (b.writes.push + b.writes.replace) fails.push(`H: a depth-2 claim with no depth-1 segment (a hole) was written: ${b.at()}`)
|
||||
shell(1, 'big-picture', false)
|
||||
if (q().get('tab') !== 'doxa-maps-spa__doxa-maps-spa' || q().get('subtab') !== 'big-picture' || b.writes.replace !== 1 || b.writes.push) fails.push(`H: a depth-1 first-paint claim is not ONE replace into subtab (the owner's tab kept): ${b.at()}`)
|
||||
shell(1, 'regions-dashboard')
|
||||
if (q().get('subtab') !== 'regions-dashboard' || b.writes.push !== 1) fails.push(`H: a depth-1 pick is not ONE push of subtab: ${b.at()}`)
|
||||
comp({ pane: 'dashboard', place: 'angola' })
|
||||
if (b.at() !== '/?tab=doxa-maps-spa__doxa-maps-spa&subtab=regions-dashboard&pane=dashboard&place=angola') fails.push(`H: a composite under a nested shell did not add pane + place beside subtab: ${b.at()}`)
|
||||
shell(1, 'regions-dashboard', false)
|
||||
if (b.at() !== '/?tab=doxa-maps-spa__doxa-maps-spa&subtab=regions-dashboard&pane=dashboard&place=angola') fails.push(`H: the same tab claimed again dropped deeper keys: ${b.at()}`)
|
||||
shell(1, 'posterverse')
|
||||
shell(2, 'country-posters', false)
|
||||
if (q().get('subtab') !== 'posterverse.country-posters' || q().get('pane') || q().get('place')) fails.push(`H: depth 2 is not the second segment, or the depth-1 change kept pane/place: ${b.at()}`)
|
||||
shell(2, 'upg-100')
|
||||
shell(3, 'map', false)
|
||||
if (q().get('subtab') !== 'posterverse.upg-100.map') fails.push(`H: depth 3 is not the third segment: ${b.at()}`)
|
||||
// a deep link's outer shell claims its (same) tab at first paint: the deeper segments must survive
|
||||
shell(1, 'posterverse', false); shell(2, 'upg-100', false)
|
||||
if (q().get('subtab') !== 'posterverse.upg-100.map') fails.push(`H: the same tab claimed again dropped deeper segments: ${b.at()}`)
|
||||
shell(2, 'poster-design')
|
||||
if (q().get('subtab') !== 'posterverse.poster-design') fails.push(`H: a depth-2 change did not drop depth 3 (or touched depth 1): ${b.at()}`)
|
||||
if (q().get('tab') !== 'doxa-maps-spa__doxa-maps-spa') fails.push(`H: a nested shell's claim changed the OWNER's tab: ${b.at()}`)
|
||||
if (va.subtabAt(location.search, 1) !== 'posterverse' || va.subtabAt(location.search, 2) !== 'poster-design' || va.subtabAt(location.search, 3) !== '') fails.push('H: subtabAt does not give each depth its own segment')
|
||||
const pushes = b.writes.push
|
||||
b.hist.back()
|
||||
if (q().get('subtab') !== 'posterverse.upg-100.map') fails.push(`H: Back did not return to the previous nested tab: ${b.at()}`)
|
||||
b.hist.forward()
|
||||
va.writeViewKeys({ tab: 'posterverse__posterverse' }, { owner: 'app' })
|
||||
if (b.at() !== '/?tab=posterverse__posterverse' || b.writes.push !== pushes + 1) fails.push(`H: the owner's tab change did not drop every subtab segment: ${b.at()}`)
|
||||
off()
|
||||
}
|
||||
{
|
||||
// a tab claim that is refused (a hole) must not take the rest of its announcement with it
|
||||
const b = browser('https://x.test/?tab=doxa-maps-spa__doxa-maps-spa')
|
||||
const el = new EventTarget()
|
||||
const off = va.ownAddress(el, () => 'app')
|
||||
el.dispatchEvent(new CustomEvent(va.VIEW_EVENT, { bubbles: true, composed: true, detail: { changes: { tab: 'x', place: 'benin' }, push: false, depth: 3 } }))
|
||||
if (b.q().get('place') !== 'benin' || b.q().get('subtab')) fails.push(`H: a refused tab claim dropped the pane/place announced with it: ${b.at()}`)
|
||||
off()
|
||||
}
|
||||
|
||||
// ── F. pane-wrapper's address, on a simulated column (the slow arm: it waits out the scroll debounce) ──
|
||||
globalThis.document = { createElement: () => new FakeEl() }
|
||||
if (!skipF) {
|
||||
|
|
@ -419,6 +474,15 @@ const src = {
|
|||
}
|
||||
const counts = census()
|
||||
const real = { va, sv, counts, pw, src }
|
||||
// useViewAddress with one line changed (it has no imports, so a sibling copy loads as is).
|
||||
async function vaVariant(from, to) {
|
||||
const f0 = join(ROOT, 'library/composables/useViewAddress.js')
|
||||
const s = readFileSync(f0, 'utf8')
|
||||
if (!s.includes(from)) throw new Error('poison anchor not found in useViewAddress.js: ' + from)
|
||||
const f = join(ROOT, `library/composables/.tmp-check-view-address-${process.pid}-${nth++}.js`)
|
||||
writeFileSync(f, s.replace(from, to))
|
||||
try { return await import(f) } finally { unlinkSync(f) }
|
||||
}
|
||||
// A pane-wrapper with one line changed, loaded from a sibling file so its relative import still resolves.
|
||||
let nth = 0
|
||||
async function pwVariant(from, to) {
|
||||
|
|
@ -469,7 +533,17 @@ if (process.argv.includes('--self-test')) {
|
|||
['pane-wrapper retrying a pending link by rAF only when there is no observer', { ...real, pw: await pwVariant("if (pending && typeof requestAnimationFrame !== 'undefined') {", "if (pending && typeof ResizeObserver === 'undefined' && typeof requestAnimationFrame !== 'undefined') {") }, /^F: with an observer that never fires again/],
|
||||
['pane-wrapper ignoring a link', { ...real, pw: await pwVariant("let pending = known(linked) && linked !== column[0].id ? linked : ''", "let pending = ''") }, /^F: a link to \?pane=dashboard did not open/],
|
||||
['pane-wrapper writing without the opt-in', { ...real, pw: await pwVariant("=> address && claimView({ pane: id === column[0].id ? '' : id, ...changes }, { owner: address.owner(),", "=> claimView({ pane: id === column[0].id ? '' : id, ...changes }, { owner: address ? address.owner() : 'app',") }, /^F: a column WITHOUT the address opt-in/],
|
||||
['ownAddress that lets a nested element switch the tab', { ...real, va: { ...va, ownAddress: (el, g) => { const h = (e) => { const o = g(); if (o) { e.stopPropagation(); va.writeViewKeys(e.detail.changes, { owner: o, el, push: e.detail.push }) } }; el.addEventListener(va.VIEW_EVENT, h); return () => el.removeEventListener(va.VIEW_EVENT, h) } } }, /^E: a nested announcement switched the OWNER's tab/],
|
||||
// P2b: nested shells at any depth
|
||||
['a nested shell\'s tab dropped (the P2 behaviour)', { ...real, va: await vaVariant(' if (tab) {', ' if (false) {') }, /^H: a depth-1 first-paint claim/],
|
||||
['a depth-2 claim that ignores its depth (overwrites segment 1)', { ...real, va: await vaVariant('path.slice(0, depth - 1).concat(tab)', '[tab]') }, /^H: depth 2 is not the second segment|^H: a depth-2 claim with no depth-1/],
|
||||
['a hole that is not refused', { ...real, va: await vaVariant('if (path.length >= depth - 1 && path[depth - 1] !== tab)', 'if (path[depth - 1] !== tab)') }, /^H: a depth-2 claim with no depth-1 segment/],
|
||||
['a refused tab claim that drops the rest of its announcement', { ...real, va: await vaVariant('if (path.length >= depth - 1 && path[depth - 1] !== tab) changes.subtab', 'if (path.length < depth - 1 || path[depth - 1] === tab) return; changes.subtab') }, /^H: a refused tab claim dropped/],
|
||||
['the same tab claimed again is not a no-op', { ...real, va: await vaVariant('if (path.length >= depth - 1 && path[depth - 1] !== tab)', 'if (path.length >= depth - 1)') }, /^H: the same tab claimed again dropped deeper/],
|
||||
['a subtab change that keeps pane and place', { ...real, va: await vaVariant('const LEVEL = { tab: 0, subtab: 1, pane: 2, place: 2 }', 'const LEVEL = { tab: 0, subtab: 2, pane: 2, place: 2 }') }, /^A: a subtab change drops pane and place|^H: depth 2 is not the second segment, or the depth-1 change kept/],
|
||||
["an owner's tab change that keeps subtab", { ...real, va: await vaVariant('const LEVEL = { tab: 0, subtab: 1, pane: 2, place: 2 }', 'const LEVEL = { tab: 0, subtab: 0, pane: 2, place: 2 }') }, /^A: a tab change drops subtab|^H: the owner's tab change did not drop/],
|
||||
['a subtab path longer than 6', { ...real, va: await vaVariant('return p.length <= MAX_DEPTH && p.every', 'return p.length <= 99 && p.every') }, /^A: a subtab path reads segment by segment/],
|
||||
['a subtab path with an empty segment', { ...real, va: await vaVariant('p.every((x) => cleanKey(x) === x && x)', 'p.every((x) => !x || cleanKey(x) === x)') }, /^A: a subtab path reads segment by segment/],
|
||||
['ownAddress that lets a nested element switch the tab', { ...real, va: { ...va, ownAddress: (el, g) => { const h = (e) => { const o = g(); if (o) { e.stopPropagation(); va.writeViewKeys(e.detail.changes, { owner: o, el, push: e.detail.push }) } }; el.addEventListener(va.VIEW_EVENT, h); return () => el.removeEventListener(va.VIEW_EVENT, h) } } }, /^H: /],
|
||||
['pane-wrapper not claiming its pane when shown again', { ...real, pw: await pwVariant("if (typeof ResizeObserver !== 'undefined') {", 'if (false) {') }, /^F: a kept-alive column shown again/],
|
||||
['pane-wrapper letting the screen beat an address that names a pane', { ...real, pw: await pwVariant('if (known(asked) && asked !== current()) show(asked, \'instant\')\n else if (!asked) claimPane', 'if (false) show(asked, \'instant\')\n else claimPane') }, /^F: shown again with an address that names a pane/],
|
||||
['pane-wrapper whose scrollTo(id, null) throws (the old default)', { ...real, pw: await pwVariant('const { write = true, push = true, changes = {} } = o || {}', 'const { write = true, push = true, changes = {} } = o === undefined ? {} : o') }, /^F: scrollTo\(id, null\)|^THREW: .*null/],
|
||||
|
|
|
|||
|
|
@ -4,8 +4,12 @@
|
|||
* link on every host, while a partner's page is never written to.
|
||||
*
|
||||
* ★ THE SCHEME (query, not hash: a hash is a host page's anchor, and filters already live there).
|
||||
* ?tab=<app tab>&pane=<view inside the tab>&place=<selection>
|
||||
* Levels: changing `tab` drops `pane` and `place`, which belong to the tab. `pane` and `place` are siblings: a
|
||||
* ?tab=<app tab>&subtab=<nested shells' tabs>&pane=<view inside the tab>&place=<selection>
|
||||
* `subtab` is a PATH, one segment per shell NESTED below the owner (Plugin Studio → the Research Gallery → …):
|
||||
* ?subtab=regions-dashboard at depth 1, ?subtab=posterverse.upg-100 when a shell sits inside a shell. A nested
|
||||
* shell's depth comes from its profile-config `addressDepth` (depthOf), which its parent shell passes down.
|
||||
* Levels: changing `tab` drops `subtab`, `pane` and `place`, which belong to the tab; changing a subtab segment drops
|
||||
* the deeper segments and `pane` and `place`. `pane` and `place` are siblings: a
|
||||
* composite keeps its selection while the reader looks at another of its panes (Regions keeps the region while the
|
||||
* reader is on the list), so the address keeps it too. Every other parameter (a host's
|
||||
* own, the filters useShareableView writes) and the hash are kept as they are. A value that is not a plain id
|
||||
|
|
@ -34,8 +38,11 @@
|
|||
* Pure helpers, no dependency. Safe under node and in a sandboxed frame (every access is guarded).
|
||||
*/
|
||||
|
||||
export const VIEW_KEYS = ['tab', 'pane', 'place']
|
||||
/** nested element → the address owner: { changes: { tab?, pane?, place? }, push }. Composed + bubbling, so it leaves
|
||||
export const VIEW_KEYS = ['tab', 'subtab', 'pane', 'place']
|
||||
const LEVEL = { tab: 0, subtab: 1, pane: 2, place: 2 }
|
||||
const MAX_DEPTH = 6
|
||||
/** nested element → the address owner: { changes: { tab?, pane?, place? }, push, depth }. A nested SHELL's tab comes
|
||||
* with its depth and is recorded as that segment of `subtab`. Composed + bubbling, so it leaves
|
||||
* shadow roots. One event per reader action, so one action is one Back step. */
|
||||
export const VIEW_EVENT = 'dxm-view'
|
||||
/** an owner with urlOwner 'host' → the host's router: { keys, href, push }; the host does router[push ? 'push' : 'replace'](href). */
|
||||
|
|
@ -43,14 +50,22 @@ export const ADDRESS_EVENT = 'dxm-address'
|
|||
|
||||
/** A plain id (letters, digits, `-`, `_`, at most 64), or '' for anything else. */
|
||||
export const cleanKey = (v) => { const s = String(v ?? '').trim(); return /^[A-Za-z0-9][\w-]{0,63}$/.test(s) ? s : '' }
|
||||
/** A subtab PATH: plain-id segments joined by '.', at most MAX_DEPTH; '' if any segment is not a plain id. */
|
||||
const cleanPath = (v) => { const p = String(v ?? '').trim().split('.'); return p.length <= MAX_DEPTH && p.every((x) => cleanKey(x) === x && x) ? p.join('.') : '' }
|
||||
const clean = (k, v) => (k === 'subtab' ? cleanPath(v) : cleanKey(v))
|
||||
/** A nested shell's depth below the address owner: profile-config addressDepth, 1 when absent. */
|
||||
export const depthOf = (cfg) => Math.min(MAX_DEPTH, Math.max(1, Math.floor(Number(cfg && cfg.addressDepth)) || 1))
|
||||
|
||||
/** { tab, pane, place } from a search string; '' for a key that is absent or not a plain id. The FIRST of a repeated
|
||||
* key decides, as URLSearchParams.get does for the shells that already read ?tab=; a malformed escape reads as absent. */
|
||||
export function readViewKeys(search) {
|
||||
const q = new URLSearchParams(search || '')
|
||||
return Object.fromEntries(VIEW_KEYS.map((k) => [k, cleanKey(q.get(k))]))
|
||||
return Object.fromEntries(VIEW_KEYS.map((k) => [k, clean(k, q.get(k))]))
|
||||
}
|
||||
|
||||
/** The tab a shell nested at `depth` should open: that segment of ?subtab=, or ''. */
|
||||
export const subtabAt = (search, depth = 1) => readViewKeys(search).subtab.split('.')[depth - 1] || ''
|
||||
|
||||
// Bubbling + composed, so the event leaves every shadow root on its way to the owner or the host.
|
||||
const send = (el, type, detail) => !!el?.dispatchEvent && (el.dispatchEvent(new CustomEvent(type, { bubbles: true, composed: true, detail })), true)
|
||||
|
||||
|
|
@ -61,10 +76,10 @@ const send = (el, type, detail) => !!el?.dispatchEvent && (el.dispatchEvent(new
|
|||
*/
|
||||
export function viewSearch(search, changes = {}) {
|
||||
const next = readViewKeys(search)
|
||||
VIEW_KEYS.forEach((k, level) => {
|
||||
if (!(k in changes) || cleanKey(changes[k]) === next[k]) return
|
||||
next[k] = cleanKey(changes[k])
|
||||
if (!level) VIEW_KEYS.slice(1).forEach((d) => { if (!(d in changes)) next[d] = '' })
|
||||
VIEW_KEYS.forEach((k) => {
|
||||
if (!(k in changes) || clean(k, changes[k]) === next[k]) return
|
||||
next[k] = clean(k, changes[k])
|
||||
VIEW_KEYS.forEach((d) => { if (LEVEL[d] > LEVEL[k] && !(d in changes)) next[d] = '' })
|
||||
})
|
||||
const all = VIEW_KEYS.filter((k) => next[k]).map((k) => k + '=' + next[k])
|
||||
.concat((search || '').replace(/^\?/, '').split('&').filter((p) => p && !VIEW_KEYS.includes(p.split('=')[0])))
|
||||
|
|
@ -108,28 +123,36 @@ export function writeViewKeys(changes, { owner = '', push = true, el = null } =
|
|||
}
|
||||
|
||||
// Only the view keys, each value cleaned: what an announcement may carry.
|
||||
const viewChanges = (changes) => Object.fromEntries(Object.entries(changes || {}).filter(([k]) => VIEW_KEYS.includes(k)).map(([k, v]) => [k, cleanKey(v)]))
|
||||
const viewChanges = (changes) => Object.fromEntries(Object.entries(changes || {}).filter(([k]) => VIEW_KEYS.includes(k)).map(([k, v]) => [k, clean(k, v)]))
|
||||
|
||||
/** A nested element reports a reader's choice to whoever owns the address (it never writes itself). */
|
||||
export function announceView(el, changes, { push = true } = {}) {
|
||||
export function announceView(el, changes, { push = true, depth = 1 } = {}) {
|
||||
const c = viewChanges(changes)
|
||||
return Object.keys(c).length > 0 && send(el, VIEW_EVENT, { changes: c, push })
|
||||
return Object.keys(c).length > 0 && send(el, VIEW_EVENT, { changes: c, push, depth })
|
||||
}
|
||||
|
||||
/** THE call for a reader's choice: the owner writes it ('written' | 'announced' | 'unchanged' | 'none'); any other
|
||||
* element announces it to the owner above it ('announced' | 'none'). */
|
||||
export const claimView = (changes, { owner = '', el = null, push = true } = {}) =>
|
||||
owner ? writeViewKeys(changes, { owner, el, push }) : (announceView(el, changes, { push }) ? 'announced' : 'none')
|
||||
export const claimView = (changes, { owner = '', el = null, push = true, depth = 1 } = {}) =>
|
||||
owner ? writeViewKeys(changes, { owner, el, push }) : (announceView(el, changes, { push, depth }) ? 'announced' : 'none')
|
||||
|
||||
/** An owner records what the elements inside it announce: their pane and place. `tab` is the owner's own key, so a
|
||||
* nested element cannot switch it. getOwner() is read per event (a host may set profile-config after mount); while
|
||||
* it is '' the event passes on to an owner further out. Returns off(). */
|
||||
/** An owner records what the elements inside it announce. `tab` is the owner's own key, so a nested element's tab
|
||||
* (a nested SHELL's) is recorded as segment `depth` of `subtab`, never as `tab`. The segments above it are kept and
|
||||
* the deeper ones dropped. The same tab again changes nothing, which keeps the deeper segments, pane and place: a
|
||||
* shell's first-paint claim on a deep link. A segment above it that is missing (a hole) is refused, so a path never
|
||||
* names the wrong depth. getOwner() is read per event (a host may set profile-config after mount); while it is ''
|
||||
* the event passes on to an owner further out. Returns off(). */
|
||||
export function ownAddress(el, getOwner) {
|
||||
const h = (e) => {
|
||||
const owner = getOwner()
|
||||
if (!owner || !e.detail) return
|
||||
e.stopPropagation()
|
||||
const { tab, ...changes } = viewChanges(e.detail.changes)
|
||||
const { tab, subtab, ...changes } = viewChanges(e.detail.changes)
|
||||
if (tab) {
|
||||
const depth = Math.min(MAX_DEPTH, Math.max(1, Math.floor(e.detail.depth) || 1))
|
||||
const path = readViewKeys(location.search).subtab.split('.').filter(Boolean)
|
||||
if (path.length >= depth - 1 && path[depth - 1] !== tab) changes.subtab = path.slice(0, depth - 1).concat(tab).join('.')
|
||||
}
|
||||
if (Object.keys(changes).length) writeViewKeys(changes, { owner, el, push: e.detail.push !== false })
|
||||
}
|
||||
el?.addEventListener?.(VIEW_EVENT, h)
|
||||
|
|
|
|||
Loading…
Reference in a new issue