| // Copyright (C) 2024 The Android Open Source Project |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| import m from 'mithril'; |
| import {assertExists, assertTrue} from '../base/assert'; |
| import {Registry} from '../base/registry'; |
| import type {PageHandler, PageManager} from '../public/page'; |
| import type {Analytics} from '../public/analytics'; |
| import {Router} from './router'; |
| import {Gate} from '../base/mithril_utils'; |
| |
| export class PageManagerImpl implements PageManager { |
| private readonly registry = new Registry<PageHandler>((x) => x.route); |
| private readonly previousPages = new Map< |
| string, |
| {page: string; subpage: string} |
| >(); |
| // Track the current page for analytics - log when it changes |
| private currentPage: string | undefined; |
| private readonly analytics: Analytics; |
| |
| constructor(analytics: Analytics) { |
| this.analytics = analytics; |
| } |
| |
| registerPage(pageHandler: PageHandler): Disposable { |
| assertTrue(/^\/\w*$/.exec(pageHandler.route) !== null); |
| // The pluginId is injected by the proxy in AppImpl / TraceImpl. If this is |
| // undefined somebody (tests) managed to call this method without proxy. |
| return this.registry.register(pageHandler); |
| } |
| |
| // Called by index.ts upon the main frame redraw callback. |
| renderPageForCurrentRoute(): m.Children { |
| const route = Router.parseFragment(location.hash); |
| |
| // Log page changes to analytics |
| if (this.currentPage !== route.page) { |
| this.currentPage = route.page; |
| setTimeout(() => this.analytics.logEvent('User Actions', route.page), 0); |
| } |
| |
| this.previousPages.set(route.page, { |
| page: route.page, |
| subpage: route.subpage, |
| }); |
| |
| // Render all pages, but display all inactive pages with display: none and |
| // avoid calling their view functions. This makes sure DOM state such as |
| // scrolling position is retained between page flips, which can be handy |
| // when quickly switching between pages that have long scrolling content |
| // such as the timeline page. |
| return Array.from(this.previousPages.entries()) |
| .map(([key, {page, subpage}]) => { |
| const maybeRenderedPage = this.renderPageForRoute(page, subpage); |
| // If either the route doesn't exist or requires a trace but the trace |
| // is not loaded, fall back on the default route. |
| const renderedPage = |
| maybeRenderedPage ?? assertExists(this.renderPageForRoute('/', '')); |
| return [key, renderedPage]; |
| }) |
| .map(([key, page]) => { |
| return m(Gate, {open: key === route.page}, page); |
| }); |
| } |
| |
| // Will return undefined if either: (1) the route does not exist; (2) the |
| // route exists, it requires a trace, but there is no trace loaded. |
| private renderPageForRoute(page: string, subpage: string) { |
| const handler = this.registry.tryGet(page); |
| if (handler === undefined) { |
| return undefined; |
| } |
| return handler.render(subpage); |
| } |
| } |