@monimesl/dot-webview 0.1.0-alpha.2 → 0.1.0-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -23,7 +23,7 @@ export declare class PageController {
23
23
  constructor(instanceId: number, id: string, options: PageLaunchOptions, viewRenderer: ViewRender$1, dispatch: Dispatch);
24
24
  readonly getState: () => PageState$1 | null;
25
25
  readonly subscribe: (listener: () => void) => (() => void);
26
- on(handler: string): (event: DotEvent$1) => void;
26
+ on(handler: string, data?: Readonly<Record<string, unknown>>): (event: DotEvent$1) => void;
27
27
  initializeState(state: PageState$1): void;
28
28
  updateState(changes: PageState$1): void;
29
29
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/store.ts","../src/page.ts","../src/renderer.ts","../src/webview.ts","../src/index.ts"],"mappings":";;UAAiB,cAAc;WACpB,WAAW;WACX,YAAY;;qBAGV,MAAM,cAAc,cAAc;;EAIjC,YAAA,OAAO;WAIV,WAAU;WAEV,YAAa;EAKtB,IAAI,OAAO;;;;KChBR,YAAY,oBAAoB,iBAAiB,OAAO;qBAEhD;;WACF;WAEA;WACA,cAAc;WACd,aAAa;EAKpB,YAAA,oBACA,YACA,SAAS,mBACT,cAAc,cACd,UAAU;WASH,gBAAQ;WAER,YAAS;EAElB,GAAG,mBAAmB,OAAO;EAS7B,gBAAgB,OAAO;EAIvB,YAAY,SAAS;;;;UC1CN;WACN,OAAO,cAAc;WACrB,eAAe,cAAc;;UAGvB;WACN;EACT,MAAM,WAAW,SAAS,MAAM,gBAAgB,YAAY;;;;KCMlD,iBAAiB,eAC3B,iBACA,qBAAqB;UAGN;EACf,SAAS,MAAM;EAEf,UAAU,MAAM;EAEhB,MAAM,gBAAgB,SAAS;;UAGhB;WACN;EAET,MAAM,SAAS;;UAGA;EACf,UAAU;EACV,WAAW;EACX,mBAAmB;EACnB,OAAO,eAAe;;;;;;;cAcX;;WACF,cAAc;WACd,SAAS;WACT,OAAK,aAAA;WACL,OAAK,MAAA;WACL,eAAa,MAAA;EASV,cAAE,WAAW,UAAU,OAAO,WAAgB;EAgC1D,cAAc,QAAQ;EAKtB;EASA,MAAM,WAAW;MAMb,kBAAkB;EAkHtB,WAAW,qBAAqB;;;;iBCrNjB,qBAAqB;EACpC,WAAW;;wBAGG,eAAe,cAAc,WAAW,eAAe"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/store.ts","../src/page.ts","../src/renderer.ts","../src/webview.ts","../src/index.ts"],"mappings":";;UAAiB,cAAc;WACpB,WAAW;WACX,YAAY;;qBAGV,MAAM,cAAc,cAAc;;EAIjC,YAAA,OAAO;WAIV,WAAU;WAEV,YAAa;EAKtB,IAAI,OAAO;;;;KChBR,YAAY,oBAAoB,iBAAiB,OAAO;qBAEhD;;WACF;WAEA;WACA,cAAc;WACd,aAAa;EAKpB,YAAA,oBACA,YACA,SAAS,mBACT,cAAc,cACd,UAAU;WASH,gBAAQ;WAER,YAAS;EAElB,GAAG,iBAAiB,OAAO,SAAS,4BAA4B,OAAO;EAYvE,gBAAgB,OAAO;EAIvB,YAAY,SAAS;;;;UC7CN;WACN,OAAO,cAAc;WACrB,eAAe,cAAc;;UAGvB;WACN;EACT,MAAM,WAAW,SAAS,MAAM,gBAAgB,YAAY;;;;KCMlD,iBAAiB,eAC3B,iBACA,qBAAqB;UAGN;EACf,SAAS,MAAM;EAEf,UAAU,MAAM;EAEhB,MAAM,gBAAgB,SAAS;;UAGhB;WACN;EAET,MAAM,SAAS;;UAGA;EACf,UAAU;EACV,WAAW;EACX,mBAAmB;EACnB,OAAO,eAAe;;;;;;;cAcX;;WACF,cAAc;WACd,SAAS;WACT,OAAK,aAAA;WACL,OAAK,MAAA;WACL,eAAa,MAAA;EASV,cAAE,WAAW,UAAU,OAAO,WAAgB;EAgC1D,cAAc,QAAQ;EAKtB;EASA,MAAM,WAAW;MAMb,kBAAkB;EAkHtB,WAAW,qBAAqB;;;;iBCrNjB,qBAAqB;EACpC,WAAW;;wBAGG,eAAe,cAAc,WAAW,eAAe"}
package/dist/index.js CHANGED
@@ -35,7 +35,11 @@ var PageController = class {
35
35
  }
36
36
  getState = this.#state.get;
37
37
  subscribe = this.#state.subscribe;
38
- on(handler) {
38
+ on(handler, data) {
39
+ if (data) return (event) => this.#dispatch(this.pageInstanceId, handler, {
40
+ ...event,
41
+ data
42
+ });
39
43
  let callback = this.#callbacks.get(handler);
40
44
  if (!callback) {
41
45
  callback = (event) => this.#dispatch(this.pageInstanceId, handler, event);
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/store.ts","../src/page.ts","../src/webview.ts","../src/index.ts"],"sourcesContent":["export interface ReadonlyStore<T> {\n readonly get: () => T;\n readonly subscribe: (listener: () => void) => () => void;\n}\n\nexport class Store<T> implements ReadonlyStore<T> {\n #value: T;\n readonly #listeners = new Set<() => void>();\n\n constructor(value: T) {\n this.#value = value;\n }\n\n readonly get = (): T => this.#value;\n\n readonly subscribe = (listener: () => void): (() => void) => {\n this.#listeners.add(listener);\n return () => this.#listeners.delete(listener);\n };\n\n set(value: T): void {\n this.#value = value;\n this.#listeners.forEach((listener) => listener());\n }\n}\n","import { Store } from './store';\nimport type { DotEvent, PageLaunchOptions, PageState, ViewRender } from '@monimesl/dot-core';\nimport { applyChanges } from '@monimesl/dot-core';\n\ntype Dispatch = (instanceId: number, handler: string, event: DotEvent) => void;\n\nexport class PageController {\n readonly pageId: string;\n readonly #dispatch: Dispatch;\n readonly pageInstanceId: number;\n readonly viewRenderer: ViewRender;\n readonly pageOptions: PageLaunchOptions;\n readonly #state = new Store<PageState | null>(null);\n readonly #callbacks = new Map<string, (event: DotEvent) => void>();\n\n constructor(\n instanceId: number,\n id: string,\n options: PageLaunchOptions,\n viewRenderer: ViewRender,\n dispatch: Dispatch,\n ) {\n this.pageId = id;\n this.pageOptions = options;\n this.#dispatch = dispatch;\n this.pageInstanceId = instanceId;\n this.viewRenderer = viewRenderer;\n }\n\n readonly getState = this.#state.get;\n\n readonly subscribe = this.#state.subscribe;\n\n on(handler: string): (event: DotEvent) => void {\n let callback = this.#callbacks.get(handler);\n if (!callback) {\n callback = (event) => this.#dispatch(this.pageInstanceId, handler, event);\n this.#callbacks.set(handler, callback);\n }\n return callback;\n }\n\n initializeState(state: PageState): void {\n this.#state.set(state);\n }\n\n updateState(changes: PageState): void {\n const state = this.#state.get();\n if (state) {\n this.#state.set(applyChanges(state, changes));\n }\n }\n}\n","import type {\n Envelope,\n HostLayerMessages,\n LogicLayerMessages,\n StackEntry,\n SystemContext,\n Theme,\n Transport,\n UILayerMessages,\n ViewRender,\n} from '@monimesl/dot-core';\nimport { DEFAULT_SYSTEM_CONTEXT, EventEmitter, MessageChannel } from '@monimesl/dot-core';\n\nimport { Store } from './store';\nimport { PageController } from './page';\nimport type { DotContext, Renderer } from './renderer';\n\nexport type WebviewChannel = MessageChannel<\n UILayerMessages,\n LogicLayerMessages & HostLayerMessages\n>;\n\nexport interface WebviewHooks {\n pageOpen(page: PageController): void;\n\n pageClose(page: PageController): void;\n\n error(error: unknown, message: Envelope): void;\n}\n\nexport interface WebviewPlugin {\n readonly name: string;\n\n setup(webview: Webview): void;\n}\n\nexport interface WebviewOptions {\n renderer: Renderer;\n transport: Transport;\n plugins?: readonly WebviewPlugin[];\n views: Record<string, ViewRender>;\n}\n\ninterface OpenPage {\n readonly page: PageController;\n readonly element: HTMLElement;\n readonly unmount: () => void;\n}\n\n/**\n * The view layer: shows the navigation stack the host announces, keeps each open page in sync with\n * the service, and hands pages to the renderer. Every open page gets its own element in the\n * container; only the top one is visible, so going back is instant and keeps its UI.\n */\nexport class Webview {\n readonly pageRenderer: Renderer;\n readonly channel: WebviewChannel;\n readonly hooks = new EventEmitter<WebviewHooks>();\n readonly theme = new Store<Theme>({});\n readonly systemContext = new Store<SystemContext>(DEFAULT_SYSTEM_CONTEXT);\n readonly #dotContext: DotContext = { theme: this.theme, systemContext: this.systemContext };\n readonly #viewByPageId: Record<string, ViewRender>;\n #openPages: OpenPage[] = [];\n // The page the service was last told is showing: the top one, while the app is active.\n #shownPage: OpenPage | undefined;\n #pending: readonly StackEntry[] = [];\n #container: Element | null = null;\n\n constructor({ transport, renderer, views, plugins = [] }: WebviewOptions) {\n this.pageRenderer = renderer;\n this.#viewByPageId = views;\n this.channel = new MessageChannel(transport);\n this.channel.handle('setTheme', ({ theme }) => {\n this.theme.set(theme);\n });\n this.channel.handle('initPageState', ({ instanceId, state }) => {\n this.locatePage(instanceId)?.initializeState(state);\n });\n this.channel.handle('setPageState', ({ instanceId, patchState }) => {\n this.locatePage(instanceId)?.updateState(patchState);\n });\n this.channel.handle('syncStack', ({ pages }) => {\n this.#reconcile(pages);\n });\n this.channel.handle('syncSystemContext', (context) => {\n this.systemContext.set(context);\n this.#showTopPage();\n });\n this.channel.handle('error', ({ instanceId, message }) => {\n if (instanceId === undefined) {\n console.error(`[dot] service error: ${message}`);\n return;\n }\n // The page may have closed before its error arrived; its instance id is all that is left.\n const pageName = this.locatePage(instanceId)?.pageId ?? `#${instanceId}`;\n console.error(`[dot] service error in page \"${pageName}\": ${message}`);\n });\n plugins.forEach((plugin) => this.preparePlugin(plugin));\n }\n\n preparePlugin(plugin: WebviewPlugin): this {\n plugin.setup(this);\n return this;\n }\n\n start(): () => void {\n const stopListening = this.channel.listen((error, message) => {\n this.hooks.emit('error', error, message);\n console.error('[dot] webview error', error);\n });\n this.channel.send('connect', {});\n return stopListening;\n }\n\n mount(container: Element): () => void {\n this.#container = container;\n this.#reconcile(this.#pending);\n return () => this.#reconcile([]);\n }\n\n get stack(): readonly PageController[] {\n return this.#openPages.map(({ page }) => page);\n }\n\n // Makes the open pages match the stack the host announced.\n #reconcile(pages: readonly StackEntry[]): void {\n if (!this.#container) {\n this.#pending = pages;\n return;\n }\n this.#closePagesNotIn(pages);\n // The page being covered hides before the new one loads, as on a device.\n if (this.#shownPage?.page.pageInstanceId !== pages.at(-1)?.instanceId) {\n this.#showOnly(undefined);\n }\n this.#openPagesIn(pages);\n const top = this.#top();\n for (const open of this.#openPages) {\n open.element.hidden = open !== top;\n }\n this.#showTopPage();\n }\n\n // The top page shows while the app is active, and hides while the person is away from Dash.\n // Its element stays visible either way, so the app switcher's snapshot shows it.\n #showTopPage(): void {\n if (this.systemContext.get().appState === 'active') {\n this.#showOnly(this.#top());\n } else {\n this.#showOnly(undefined);\n }\n }\n\n #showOnly(open: OpenPage | undefined): void {\n if (open === this.#shownPage) {\n return;\n }\n if (this.#shownPage) {\n this.channel.send('hide', { instanceId: this.#shownPage.page.pageInstanceId });\n }\n this.#shownPage = open;\n if (open) {\n this.channel.send('show', { instanceId: open.page.pageInstanceId });\n }\n }\n\n #closePagesNotIn(pages: readonly StackEntry[]): void {\n const instanceIdsToKeep = new Set(pages.map((entry) => entry.instanceId));\n // Close from the top of the stack down, the order a device closes pages in.\n for (const open of [...this.#openPages].reverse()) {\n if (!instanceIdsToKeep.has(open.page.pageInstanceId)) {\n this.#close(open);\n }\n }\n }\n\n #openPagesIn(pages: readonly StackEntry[]): void {\n const kept = new Map(this.#openPages.map((open) => [open.page.pageInstanceId, open]));\n const opened: OpenPage[] = [];\n this.#openPages = pages.map((entry) => {\n let open = kept.get(entry.instanceId);\n if (!open) {\n open = this.#open(entry);\n opened.push(open);\n }\n return open;\n });\n // Only now, with every page in the stack, can the service's replies find their page.\n for (const { page } of opened) {\n this.channel.send('load', {\n instanceId: page.pageInstanceId,\n page: page.pageId,\n options: page.pageOptions,\n });\n }\n }\n\n #open({ instanceId, page: id, options }: StackEntry): OpenPage {\n const view = this.#viewByPageId[id];\n if (!view) {\n throw new Error(`Unknown page \"${id}\"`);\n }\n const page = new PageController(instanceId, id, options, view, (instanceId, handler, event) =>\n this.channel.send('event', { instanceId, handler, event }),\n );\n const element = this.#container!.ownerDocument.createElement('div');\n element.dataset.page = id;\n element.hidden = true;\n this.#container!.append(element);\n const open: OpenPage = {\n page,\n element,\n unmount: this.pageRenderer.mount(element, page, this.#dotContext),\n };\n this.hooks.emit('pageOpen', page);\n return open;\n }\n\n #close(open: OpenPage): void {\n // A closing page unloads rather than hides.\n if (this.#shownPage === open) {\n this.#shownPage = undefined;\n }\n open.unmount();\n open.element.remove();\n this.#openPages = this.#openPages.filter((other) => other !== open);\n this.channel.send('unload', { instanceId: open.page.pageInstanceId });\n this.hooks.emit('pageClose', open.page);\n }\n\n #top(): OpenPage | undefined {\n return this.#openPages.at(-1);\n }\n\n locatePage(instanceId: number): PageController | undefined {\n return this.#openPages.find(({ page }) => page.pageInstanceId === instanceId)?.page;\n }\n}\n","export { Store } from './store';\nimport type { WebviewOptions } from './webview';\nimport { Webview } from './webview';\n\nexport { PageController } from './page';\nexport type { ReadonlyStore } from './store';\n\nexport { hostTransport } from '@monimesl/dot-core';\nexport type { DotContext, Renderer } from './renderer';\nexport type { WebviewHooks, WebviewOptions, WebviewPlugin, WebviewChannel } from './webview';\nexport type {\n ColorScheme,\n DotEvent,\n PageState,\n RenderContext,\n SystemContext,\n Theme,\n Transport,\n ViewRender,\n} from '@monimesl/dot-core';\n\nexport interface StartOptions extends WebviewOptions {\n container: Element;\n}\n\nexport function startWebview({ container, ...options }: StartOptions): Webview {\n const webview = new Webview(options);\n webview.start();\n webview.mount(container);\n return webview;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/store.ts","../src/page.ts","../src/webview.ts","../src/index.ts"],"sourcesContent":["export interface ReadonlyStore<T> {\n readonly get: () => T;\n readonly subscribe: (listener: () => void) => () => void;\n}\n\nexport class Store<T> implements ReadonlyStore<T> {\n #value: T;\n readonly #listeners = new Set<() => void>();\n\n constructor(value: T) {\n this.#value = value;\n }\n\n readonly get = (): T => this.#value;\n\n readonly subscribe = (listener: () => void): (() => void) => {\n this.#listeners.add(listener);\n return () => this.#listeners.delete(listener);\n };\n\n set(value: T): void {\n this.#value = value;\n this.#listeners.forEach((listener) => listener());\n }\n}\n","import { Store } from './store';\nimport type { DotEvent, PageLaunchOptions, PageState, ViewRender } from '@monimesl/dot-core';\nimport { applyChanges } from '@monimesl/dot-core';\n\ntype Dispatch = (instanceId: number, handler: string, event: DotEvent) => void;\n\nexport class PageController {\n readonly pageId: string;\n readonly #dispatch: Dispatch;\n readonly pageInstanceId: number;\n readonly viewRenderer: ViewRender;\n readonly pageOptions: PageLaunchOptions;\n readonly #state = new Store<PageState | null>(null);\n readonly #callbacks = new Map<string, (event: DotEvent) => void>();\n\n constructor(\n instanceId: number,\n id: string,\n options: PageLaunchOptions,\n viewRenderer: ViewRender,\n dispatch: Dispatch,\n ) {\n this.pageId = id;\n this.pageOptions = options;\n this.#dispatch = dispatch;\n this.pageInstanceId = instanceId;\n this.viewRenderer = viewRenderer;\n }\n\n readonly getState = this.#state.get;\n\n readonly subscribe = this.#state.subscribe;\n\n on(handler: string, data?: Readonly<Record<string, unknown>>): (event: DotEvent) => void {\n if (data) {\n return (event) => this.#dispatch(this.pageInstanceId, handler, { ...event, data });\n }\n let callback = this.#callbacks.get(handler);\n if (!callback) {\n callback = (event) => this.#dispatch(this.pageInstanceId, handler, event);\n this.#callbacks.set(handler, callback);\n }\n return callback;\n }\n\n initializeState(state: PageState): void {\n this.#state.set(state);\n }\n\n updateState(changes: PageState): void {\n const state = this.#state.get();\n if (state) {\n this.#state.set(applyChanges(state, changes));\n }\n }\n}\n","import type {\n Envelope,\n HostLayerMessages,\n LogicLayerMessages,\n StackEntry,\n SystemContext,\n Theme,\n Transport,\n UILayerMessages,\n ViewRender,\n} from '@monimesl/dot-core';\nimport { DEFAULT_SYSTEM_CONTEXT, EventEmitter, MessageChannel } from '@monimesl/dot-core';\n\nimport { Store } from './store';\nimport { PageController } from './page';\nimport type { DotContext, Renderer } from './renderer';\n\nexport type WebviewChannel = MessageChannel<\n UILayerMessages,\n LogicLayerMessages & HostLayerMessages\n>;\n\nexport interface WebviewHooks {\n pageOpen(page: PageController): void;\n\n pageClose(page: PageController): void;\n\n error(error: unknown, message: Envelope): void;\n}\n\nexport interface WebviewPlugin {\n readonly name: string;\n\n setup(webview: Webview): void;\n}\n\nexport interface WebviewOptions {\n renderer: Renderer;\n transport: Transport;\n plugins?: readonly WebviewPlugin[];\n views: Record<string, ViewRender>;\n}\n\ninterface OpenPage {\n readonly page: PageController;\n readonly element: HTMLElement;\n readonly unmount: () => void;\n}\n\n/**\n * The view layer: shows the navigation stack the host announces, keeps each open page in sync with\n * the service, and hands pages to the renderer. Every open page gets its own element in the\n * container; only the top one is visible, so going back is instant and keeps its UI.\n */\nexport class Webview {\n readonly pageRenderer: Renderer;\n readonly channel: WebviewChannel;\n readonly hooks = new EventEmitter<WebviewHooks>();\n readonly theme = new Store<Theme>({});\n readonly systemContext = new Store<SystemContext>(DEFAULT_SYSTEM_CONTEXT);\n readonly #dotContext: DotContext = { theme: this.theme, systemContext: this.systemContext };\n readonly #viewByPageId: Record<string, ViewRender>;\n #openPages: OpenPage[] = [];\n // The page the service was last told is showing: the top one, while the app is active.\n #shownPage: OpenPage | undefined;\n #pending: readonly StackEntry[] = [];\n #container: Element | null = null;\n\n constructor({ transport, renderer, views, plugins = [] }: WebviewOptions) {\n this.pageRenderer = renderer;\n this.#viewByPageId = views;\n this.channel = new MessageChannel(transport);\n this.channel.handle('setTheme', ({ theme }) => {\n this.theme.set(theme);\n });\n this.channel.handle('initPageState', ({ instanceId, state }) => {\n this.locatePage(instanceId)?.initializeState(state);\n });\n this.channel.handle('setPageState', ({ instanceId, patchState }) => {\n this.locatePage(instanceId)?.updateState(patchState);\n });\n this.channel.handle('syncStack', ({ pages }) => {\n this.#reconcile(pages);\n });\n this.channel.handle('syncSystemContext', (context) => {\n this.systemContext.set(context);\n this.#showTopPage();\n });\n this.channel.handle('error', ({ instanceId, message }) => {\n if (instanceId === undefined) {\n console.error(`[dot] service error: ${message}`);\n return;\n }\n // The page may have closed before its error arrived; its instance id is all that is left.\n const pageName = this.locatePage(instanceId)?.pageId ?? `#${instanceId}`;\n console.error(`[dot] service error in page \"${pageName}\": ${message}`);\n });\n plugins.forEach((plugin) => this.preparePlugin(plugin));\n }\n\n preparePlugin(plugin: WebviewPlugin): this {\n plugin.setup(this);\n return this;\n }\n\n start(): () => void {\n const stopListening = this.channel.listen((error, message) => {\n this.hooks.emit('error', error, message);\n console.error('[dot] webview error', error);\n });\n this.channel.send('connect', {});\n return stopListening;\n }\n\n mount(container: Element): () => void {\n this.#container = container;\n this.#reconcile(this.#pending);\n return () => this.#reconcile([]);\n }\n\n get stack(): readonly PageController[] {\n return this.#openPages.map(({ page }) => page);\n }\n\n // Makes the open pages match the stack the host announced.\n #reconcile(pages: readonly StackEntry[]): void {\n if (!this.#container) {\n this.#pending = pages;\n return;\n }\n this.#closePagesNotIn(pages);\n // The page being covered hides before the new one loads, as on a device.\n if (this.#shownPage?.page.pageInstanceId !== pages.at(-1)?.instanceId) {\n this.#showOnly(undefined);\n }\n this.#openPagesIn(pages);\n const top = this.#top();\n for (const open of this.#openPages) {\n open.element.hidden = open !== top;\n }\n this.#showTopPage();\n }\n\n // The top page shows while the app is active, and hides while the person is away from Dash.\n // Its element stays visible either way, so the app switcher's snapshot shows it.\n #showTopPage(): void {\n if (this.systemContext.get().appState === 'active') {\n this.#showOnly(this.#top());\n } else {\n this.#showOnly(undefined);\n }\n }\n\n #showOnly(open: OpenPage | undefined): void {\n if (open === this.#shownPage) {\n return;\n }\n if (this.#shownPage) {\n this.channel.send('hide', { instanceId: this.#shownPage.page.pageInstanceId });\n }\n this.#shownPage = open;\n if (open) {\n this.channel.send('show', { instanceId: open.page.pageInstanceId });\n }\n }\n\n #closePagesNotIn(pages: readonly StackEntry[]): void {\n const instanceIdsToKeep = new Set(pages.map((entry) => entry.instanceId));\n // Close from the top of the stack down, the order a device closes pages in.\n for (const open of [...this.#openPages].reverse()) {\n if (!instanceIdsToKeep.has(open.page.pageInstanceId)) {\n this.#close(open);\n }\n }\n }\n\n #openPagesIn(pages: readonly StackEntry[]): void {\n const kept = new Map(this.#openPages.map((open) => [open.page.pageInstanceId, open]));\n const opened: OpenPage[] = [];\n this.#openPages = pages.map((entry) => {\n let open = kept.get(entry.instanceId);\n if (!open) {\n open = this.#open(entry);\n opened.push(open);\n }\n return open;\n });\n // Only now, with every page in the stack, can the service's replies find their page.\n for (const { page } of opened) {\n this.channel.send('load', {\n instanceId: page.pageInstanceId,\n page: page.pageId,\n options: page.pageOptions,\n });\n }\n }\n\n #open({ instanceId, page: id, options }: StackEntry): OpenPage {\n const view = this.#viewByPageId[id];\n if (!view) {\n throw new Error(`Unknown page \"${id}\"`);\n }\n const page = new PageController(instanceId, id, options, view, (instanceId, handler, event) =>\n this.channel.send('event', { instanceId, handler, event }),\n );\n const element = this.#container!.ownerDocument.createElement('div');\n element.dataset.page = id;\n element.hidden = true;\n this.#container!.append(element);\n const open: OpenPage = {\n page,\n element,\n unmount: this.pageRenderer.mount(element, page, this.#dotContext),\n };\n this.hooks.emit('pageOpen', page);\n return open;\n }\n\n #close(open: OpenPage): void {\n // A closing page unloads rather than hides.\n if (this.#shownPage === open) {\n this.#shownPage = undefined;\n }\n open.unmount();\n open.element.remove();\n this.#openPages = this.#openPages.filter((other) => other !== open);\n this.channel.send('unload', { instanceId: open.page.pageInstanceId });\n this.hooks.emit('pageClose', open.page);\n }\n\n #top(): OpenPage | undefined {\n return this.#openPages.at(-1);\n }\n\n locatePage(instanceId: number): PageController | undefined {\n return this.#openPages.find(({ page }) => page.pageInstanceId === instanceId)?.page;\n }\n}\n","export { Store } from './store';\nimport type { WebviewOptions } from './webview';\nimport { Webview } from './webview';\n\nexport { PageController } from './page';\nexport type { ReadonlyStore } from './store';\n\nexport { hostTransport } from '@monimesl/dot-core';\nexport type { DotContext, Renderer } from './renderer';\nexport type { WebviewHooks, WebviewOptions, WebviewPlugin, WebviewChannel } from './webview';\nexport type {\n ColorScheme,\n DotEvent,\n PageState,\n RenderContext,\n SystemContext,\n Theme,\n Transport,\n ViewRender,\n} from '@monimesl/dot-core';\n\nexport interface StartOptions extends WebviewOptions {\n container: Element;\n}\n\nexport function startWebview({ container, ...options }: StartOptions): Webview {\n const webview = new Webview(options);\n webview.start();\n webview.mount(container);\n return webview;\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monimesl/dot-webview",
3
- "version": "0.1.0-alpha.2",
3
+ "version": "0.1.0-alpha.3",
4
4
  "description": "Dot view layer: keeps pages in sync with the logic and hands them to a renderer",
5
5
  "type": "module",
6
6
  "repository": {
@@ -24,7 +24,7 @@
24
24
  "main": "./dist/index.js",
25
25
  "types": "./dist/index.d.ts",
26
26
  "dependencies": {
27
- "@monimesl/dot-core": "0.1.0-alpha.2"
27
+ "@monimesl/dot-core": "0.1.0-alpha.3"
28
28
  },
29
29
  "devDependencies": {
30
30
  "jsdom": "^30.1.1",