chromium/ash/webui/camera_app_ui/resources/js/nav.ts

// Copyright 2013 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 './assert.js';
import {DEPLOYED_VERSION} from './deployed_version.js';
import {toggleExpertMode} from './expert.js';
import * as state from './state.js';
import * as toast from './toast.js';
import {ViewName} from './type.js';
import * as util from './util.js';
import {EnterOptions, LeaveCondition, View} from './views/view.js';
import {windowController} from './window_controller.js';

/**
 * All views stacked in ascending z-order (DOM order) for navigation, and only
 * the topmost shown view is active (clickable/focusable).
 */
let allViews: View[] = [];

/**
 * Index of the current topmost shown view in the stacked views.
 */
let topmostIndex = -1;

/**
 * Gets view and all recursive subviews.
 */
function* getRecursiveViews(view: View): Generator<View> {
  yield view;
  for (const subview of view.getSubViews()) {
    yield* getRecursiveViews(subview);
  }
}

/**
 * Sets up navigation for all views, e.g. camera-view, dialog-view, etc.
 *
 * @param views All views in ascending z-order.
 */
export function setup(views: View[]): void {
  allViews = views.flatMap((v) => [...getRecursiveViews(v)]);
  document.addEventListener('pointerdown', () => {
    state.set(state.State.KEYBOARD_NAVIGATION, false);
  });
  document.addEventListener('keydown', () => {
    state.set(state.State.KEYBOARD_NAVIGATION, true);
  });
}

/**
 * Checks if the view is already shown.
 *
 * @param index Index of the view.
 */
function isShown(index: number): boolean {
  return state.get(allViews[index].name);
}

/**
 * Shows the view indexed in the stacked views and activates the view only if
 * it becomes the topmost shown view.
 *
 * @param index Index of the view.
 */
function show(index: number): View {
  const view = allViews[index];
  if (isShown(index)) {
    return view;
  }
  state.set(view.name, true);
  view.layout();
  if (index > topmostIndex) {
    if (topmostIndex >= 0) {
      allViews[topmostIndex].onCoveredAsTop();
    }
    topmostIndex = index;
    allViews[index].onShownAsTop();
  }
  return view;
}

/**
 * Finds the next topmost shown view in the stacked views.
 *
 * @return Index of the view found; otherwise, -1.
 */
function findNextTopmostIndex(): number {
  for (let i = topmostIndex - 1; i >= 0; i--) {
    if (isShown(i)) {
      return i;
    }
  }
  return -1;
}

/**
 * Hides the view indexed in the stacked views and deactivate the view if it was
 * the topmost shown view.
 *
 * @param index Index of the view.
 */
function hide(index: number) {
  assert(isShown(index));
  if (index === topmostIndex) {
    allViews[index].onHideAsTop();
    const next = findNextTopmostIndex();
    topmostIndex = next;
    if (next >= 0) {
      allViews[next].onUncoveredAsTop(allViews[index].name);
    }
  }
  state.set(allViews[index].name, false);
}

/**
 * Finds the view by its name in the stacked views.
 *
 * @param name View name.
 * @return Index of the view found; otherwise, -1.
 */
function findIndex(name: ViewName): number {
  return allViews.findIndex((view) => view.name === name);
}

/**
 * Opens a navigation session of the view; shows the view before entering it and
 * hides the view after leaving it for the ended session.
 *
 * The Warning view can be opened multiple times with different warning types
 * before being closed. `hide` might be called multiple times at the time the
 * Warning view is closed (no remaining warning types).
 *
 * @param name View name.
 * @param options Optional rest parameters for entering the view.
 * @return Promise for the operation or result.
 */
export function open(
    name: ViewName, options?: EnterOptions): {closed: Promise<LeaveCondition>} {
  const index = findIndex(name);
  const view = show(index);
  return {
    closed: view.enter(options).finally(() => {
      if (isShown(index)) {
        hide(index);
      }
    }),
  };
}

/**
 * Closes the current navigation session of the view by leaving it.
 *
 * @param name View name.
 * @param condition Optional condition for leaving the view.
 */
export function close(name: ViewName, condition?: unknown): void {
  const index = findIndex(name);
  allViews[index].leave({kind: 'CLOSED', val: condition});
}

/**
 * Handles key pressed event.
 */
export function onKeyPressed(event: KeyboardEvent): void {
  const key = util.getKeyboardShortcut(event);
  switch (key) {
    case 'BrowserBack':
      // Only works for non-intent instance.
      if (!state.get(state.State.INTENT)) {
        // This is used in keypress event handler, and we don't wait for the
        // window to minimize here.
        void windowController.minimize();
      }
      break;
    case 'Alt--':
      // Prevent intent window from minimizing.
      if (state.get(state.State.INTENT)) {
        event.preventDefault();
      }
      break;
    case 'Ctrl-=':
    case 'Ctrl--':
      // Blocks the in-app zoom in/out to avoid unexpected layout.
      event.preventDefault();
      break;
    case 'Ctrl-V':
      toast.showDebugMessage(`SWA${
          DEPLOYED_VERSION === undefined ?
              '' :
              `, Local overrde enabled (${DEPLOYED_VERSION})`}`);
      break;
    case 'Ctrl-Shift-E':
      toggleExpertMode();
      break;
    default:
      // Make the topmost shown view handle the pressed key.
      if (topmostIndex >= 0 && allViews[topmostIndex].onKeyPressed(key)) {
        event.preventDefault();
      }
  }
}

/**
 * Relayout all shown views.
 *
 * All shown views need being relayout after window is resized or state
 * changed.
 */
export function layoutShownViews(): void {
  for (let i = allViews.length - 1; i >= 0; i--) {
    if (isShown(i)) {
      allViews[i].layout();
    }
  }
}

/**
 * Returns whether the view is the top view above all shown view.
 */
export function isTopMostView(name: ViewName): boolean {
  return topmostIndex === findIndex(name);
}