chromium/chrome/browser/resources/welcome/router.ts

// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import {assert} from 'chrome://resources/js/assert.js';

import type {NavigationMixinInterface} from './navigation_mixin.js';

/**
 * Valid route pathnames.
 */
export enum Routes {
  LANDING = 'landing',
  NEW_USER = 'new-user',
  RETURNING_USER = 'returning-user'
}

export const routeObservers: Set<NavigationMixinInterface> = new Set();

let currentRouteElement: NavigationMixinInterface|null;

export function setCurrentRouteElement(element: NavigationMixinInterface) {
  currentRouteElement = element;
}

// Notifies all the elements that extended NavigationMixin.
function notifyObservers() {
  if (currentRouteElement) {
    currentRouteElement.onRouteExit();
    currentRouteElement = null;
  }
  const route = (history.state.route as Routes);
  const step = history.state.step;
  routeObservers.forEach(observer => {
    observer.onRouteChange(route, step);

    // Modules are only attached to DOM if they're for the current route, so
    // as long as the id of an element matches up to the current step, it
    // means that element is for the current route.
    if ((observer as unknown as HTMLElement).id === `step-${step}`) {
      currentRouteElement = observer;
    }
  });

  // If currentRouteElement is not null, it means there was a new route.
  if (currentRouteElement) {
    (currentRouteElement as NavigationMixinInterface).notifyRouteEnter();
  }
}

export function navigateToNextStep() {
  history.pushState(
      {route: history.state.route, step: history.state.step + 1}, '',
      `/${history.state.route}`);
  notifyObservers();
}

export function navigateTo(route: Routes, step: number) {
  assert([
    Routes.LANDING,
    Routes.NEW_USER,
    Routes.RETURNING_USER,
  ].includes(route));

  history.pushState(
      {
        route: route,
        step: step,
      },
      '', '/' + (route === Routes.LANDING ? '' : route));
  notifyObservers();
}

function main() {
  /**
   * Regular expression that captures the leading slash, the content and the
   * trailing slash in three different groups.
   */
  const CANONICAL_PATH_REGEX: RegExp = /(^\/)([\/-\w]+)(\/$)/;
  const path = location.pathname.replace(CANONICAL_PATH_REGEX, '$1$2');

  // Sets up history state based on the url path, unless it's already set (e.g.
  // when user uses browser-back button to get back on chrome://welcome/...).
  if (!history.state || !history.state.route || !history.state.step) {
    switch (path) {
      case `/${Routes.NEW_USER}`:
        history.replaceState({route: Routes.NEW_USER, step: 1}, '', path);
        break;
      case `/${Routes.RETURNING_USER}`:
        history.replaceState({route: Routes.RETURNING_USER, step: 1}, '', path);
        break;
      default:
        history.replaceState(
            {route: Routes.LANDING, step: Routes.LANDING}, '', '/');
    }
  }

  // Notifies all elements when browser history is popped.
  window.addEventListener('popstate', notifyObservers);

  // Notify the active element before unload.
  window.addEventListener('beforeunload', () => {
    if (currentRouteElement) {
      (currentRouteElement as {onRouteUnload: Function}).onRouteUnload();
    }
  });
}
main();