72 lines
1.9 KiB
TypeScript
72 lines
1.9 KiB
TypeScript
|
|
let nextScopeId = 0
|
||
|
|
let nextLayerOrder = 0
|
||
|
|
|
||
|
|
export interface CreateOverlayScopeOptions {
|
||
|
|
name?: string
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface OverlayLayer {
|
||
|
|
dismiss: () => void
|
||
|
|
dismissible: boolean
|
||
|
|
id: string
|
||
|
|
order: number
|
||
|
|
}
|
||
|
|
|
||
|
|
/** UI-agnostic storage shared by independently imported overlay features. */
|
||
|
|
export class OverlayScope {
|
||
|
|
readonly id: string
|
||
|
|
readonly name?: string
|
||
|
|
|
||
|
|
private readonly channels = new Map<symbol, unknown>()
|
||
|
|
private readonly layers = new Map<string, OverlayLayer>()
|
||
|
|
|
||
|
|
constructor({ name }: CreateOverlayScopeOptions = {}) {
|
||
|
|
this.id = `overlay-scope-${nextScopeId++}`
|
||
|
|
this.name = name
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @internal Used by atomic overlay feature modules. */
|
||
|
|
getChannel<Channel>(key: symbol, create: () => Channel): Channel {
|
||
|
|
const existing = this.channels.get(key)
|
||
|
|
if (existing) return existing as Channel
|
||
|
|
|
||
|
|
const channel = create()
|
||
|
|
this.channels.set(key, channel)
|
||
|
|
return channel
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @internal Registers a visible layer for navigation interception. */
|
||
|
|
registerLayer(layer: Omit<OverlayLayer, "order">) {
|
||
|
|
const registered = { ...layer, order: nextLayerOrder++ }
|
||
|
|
this.layers.set(registered.id, registered)
|
||
|
|
return registered.order
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @internal Removes a layer after it has settled. */
|
||
|
|
unregisterLayer(id: string) {
|
||
|
|
this.layers.delete(id)
|
||
|
|
}
|
||
|
|
|
||
|
|
getTopmostLayer() {
|
||
|
|
let topmost: OverlayLayer | undefined
|
||
|
|
for (const layer of this.layers.values()) {
|
||
|
|
if (!topmost || layer.order > topmost.order) topmost = layer
|
||
|
|
}
|
||
|
|
return topmost
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Cancels navigation while a layer is open and dismisses it when allowed. */
|
||
|
|
interceptNavigation() {
|
||
|
|
const layer = this.getTopmostLayer()
|
||
|
|
if (!layer) return false
|
||
|
|
if (layer.dismissible) layer.dismiss()
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export function createOverlayScope(options?: CreateOverlayScopeOptions) {
|
||
|
|
return new OverlayScope(options)
|
||
|
|
}
|
||
|
|
|
||
|
|
export const globalOverlayScope = createOverlayScope({ name: "global" })
|