chromium/ash/webui/common/resources/sea_pen/sea_pen_router_element.ts

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

import 'chrome://resources/ash/common/cr_elements/cr_dialog/cr_dialog.js';
import 'chrome://resources/polymer/v3_0/iron-location/iron-location.js';
import 'chrome://resources/polymer/v3_0/iron-location/iron-query-params.js';
import './sea_pen_freeform_element.js';
import './sea_pen_images_element.js';
import './sea_pen_input_query_element.js';
import './sea_pen_introduction_dialog_element.js';
import './sea_pen_recent_wallpapers_element.js';
import './sea_pen_samples_element.js';
import './sea_pen_template_query_element.js';
import './sea_pen_templates_element.js';
import './sea_pen_toast_element.js';

import {assert} from 'chrome://resources/js/assert.js';
import {afterNextRender} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';

import {QUERY, Query} from './constants.js';
import {isSeaPenEnabled, isSeaPenTextInputEnabled} from './load_time_booleans.js';
import {cleanUpSeaPenQueryStates, closeSeaPenIntroductionDialog, getShouldShowSeaPenIntroductionDialog} from './sea_pen_controller.js';
import {SeaPenTemplateId} from './sea_pen_generated.mojom-webui.js';
import {getSeaPenProvider} from './sea_pen_interface_provider.js';
import {logSeaPenVisited} from './sea_pen_metrics_logger.js';
import {SeaPenObserver} from './sea_pen_observer.js';
import {getTemplate} from './sea_pen_router_element.html.js';
import {WithSeaPenStore} from './sea_pen_store.js';
import {SeaPenTemplateQueryElement} from './sea_pen_template_query_element.js';
import {getTemplateIdFromString} from './sea_pen_utils.js';
import {maybeDoPageTransition} from './transition.js';

export enum SeaPenPaths {
  TEMPLATES = '',
  RESULTS = '/results',
  FREEFORM = '/freeform',
}

export interface SeaPenQueryParams {
  seaPenTemplateId?: string;
}

let instance: SeaPenRouterElement|null = null;

export class SeaPenRouterElement extends WithSeaPenStore {
  static get is() {
    return 'sea-pen-router';
  }
  static get template() {
    return getTemplate();
  }
  static get properties() {
    return {
      basePath: String,

      path_: String,

      query_: String,

      queryParams_: Object,

      relativePath_: {
        type: String,
        computed: 'computeRelativePath_(path_, basePath)',
        observer: 'onRelativePathChanged_',
      },

      showSeaPenIntroductionDialog_: Boolean,
    };
  }

  static instance(): SeaPenRouterElement {
    assert(instance, 'sea pen router does not exist');
    return instance;
  }

  basePath: string;
  private path_: string;
  private query_: string;
  private queryParams_: SeaPenQueryParams;
  private relativePath_: string|null;
  private showSeaPenIntroductionDialog_: boolean;

  override connectedCallback() {
    assert(isSeaPenEnabled(), 'sea pen must be enabled');
    super.connectedCallback();
    instance = this;
    this.watch<SeaPenRouterElement['showSeaPenIntroductionDialog_']>(
        'showSeaPenIntroductionDialog_',
        state => state.shouldShowSeaPenIntroductionDialog);
    this.updateFromStore();
    this.fetchIntroductionDialogStatus();
    logSeaPenVisited(this.relativePath_ as SeaPenPaths);
    afterNextRender(this, () => SeaPenObserver.initSeaPenObserverIfNeeded());
  }

  override disconnectedCallback() {
    super.disconnectedCallback();
    instance = null;
  }

  selectSeaPenTemplate(templateId: SeaPenTemplateId|Query|undefined) {
    if (templateId === undefined) {
      return;
    }
    // Clean up the Sea Pen states such as thumbnail response status code,
    // thumbnail loading status and Sea Pen query when
    // switching template; otherwise, states from the last query search will
    // remain in sea-pen-images element.
    cleanUpSeaPenQueryStates(this.getStore());
    if (templateId === QUERY) {
      this.goToRoute(SeaPenPaths.FREEFORM);
      return;
    }
    this.goToRoute(
        SeaPenPaths.RESULTS, {seaPenTemplateId: templateId.toString()});
  }

  async goToRoute(
      relativePath: SeaPenPaths, queryParams: SeaPenQueryParams = {}) {
    assert(typeof this.basePath === 'string', 'basePath must be set');
    const routingPath = this.basePath + relativePath;
    // Skip page transition animation if no changes in routing path.
    if (this.path_ === routingPath) {
      this.setProperties({queryParams_: queryParams});
      return Promise.resolve();
    }
    return maybeDoPageTransition(
        () => this.setProperties(
            {path_: routingPath, queryParams_: queryParams}));
  }

  /**
   * Compute the relative path compared to the SeaPen base path.
   * @param path the absolute path of the current route
   * @param basePath the absolute path of the base seapen route
   * @returns path relative to basePath, or null if path is not relative to
   *     basePath
   * @example
   * computeRelativePath_('/wallpaper/sea_pen', '/wallpaper/sea_pen') => ''
   * computeRelativePath_('/wallpaper/sea_pen/results', '/wallpaper/sea_pen') =>
   *   '/results'
   * computeRelativePath_('/wallpaper', '/wallpaper/sea_pen') => null
   */
  private computeRelativePath_(path: string|null, basePath: string|null): string
      |null {
    if (typeof path !== 'string' || typeof basePath !== 'string') {
      return null;
    }
    if (!path.startsWith(basePath)) {
      return null;
    }
    const relativePath = path.substring(basePath.length);
    // Normalize single slash to empty string.
    // This keeps path consistent between chrome://vc-background/ and
    // chrome://personalization/wallpaper/sea-pen.
    return relativePath === '/' ? '' : relativePath;
  }

  private onRelativePathChanged_(relativePath: string|null) {
    if (typeof relativePath !== 'string') {
      // `relativePath` will be null when using Personalization breadcrumbs to
      // navigate back to home or wallpaper. Don't reset the path, as
      // `SeaPenRouter` may be imminently torn down.
      return;
    }
    if (!Object.values(SeaPenPaths).includes(relativePath as SeaPenPaths)) {
      // If arriving at an unknown path, go back to the root path.
      console.warn('SeaPenRouter unknown path', relativePath);
      this.goToRoute(SeaPenPaths.TEMPLATES);
    }
  }

  private shouldShowTemplateQuery_(
      relativePath: string|null, templateId: string|null): boolean {
    return relativePath === SeaPenPaths.RESULTS &&
        (!!templateId && templateId !== 'Query');
  }

  private shouldShowSeaPenTemplates_(relativePath: string|null): boolean {
    if (typeof relativePath !== 'string') {
      return false;
    }
    return relativePath === SeaPenPaths.TEMPLATES;
  }

  private shouldShowSeaPenTemplateImages_(relativePath: string|null): boolean {
    if (typeof relativePath !== 'string') {
      return false;
    }
    return relativePath === SeaPenPaths.RESULTS;
  }

  private shouldShowSeaPenFreeform_(relativePath: string|null): boolean {
    return isSeaPenTextInputEnabled() && relativePath === SeaPenPaths.FREEFORM;
  }

  private onBottomContainerClicked_(): void {
    // close the chip option selection if it is open (or selected chip state is
    // set).
    this.shadowRoot!
        .querySelector<SeaPenTemplateQueryElement>('sea-pen-template-query')
        ?.onOptionSelectionDone();
  }

  private getTemplateIdFromQueryParams_(templateId: string): SeaPenTemplateId
      |Query {
    return getTemplateIdFromString(templateId);
  }

  private async fetchIntroductionDialogStatus() {
    await getShouldShowSeaPenIntroductionDialog(
        getSeaPenProvider(), this.getStore());
  }

  private async onCloseSeaPenIntroductionDialog_() {
    await closeSeaPenIntroductionDialog(getSeaPenProvider(), this.getStore());
    // Freeform focus goes to the text input automatically.
    if (this.relativePath_ !== SeaPenPaths.FREEFORM) {
      this.focusOnFirstTemplate_();
    }
  }

  private onRecentTemplateImageDelete_() {
    // focus on the first template if the deleted recent image is the only image
    // or the last image of recent images list.
    this.focusOnFirstTemplate_();
  }

  private focusOnFirstTemplate_() {
    const seaPenTemplates =
        this.shadowRoot!.querySelector<HTMLElement>('sea-pen-templates');
    const firstTemplate =
        seaPenTemplates!.shadowRoot!.querySelector<HTMLElement>(
            '.sea-pen-template');
    window.scrollTo(0, 0);
    firstTemplate!.focus();
  }
}

declare global {
  interface HTMLElementTagNameMap {
    'sea-pen-router': SeaPenRouterElement;
  }
}

customElements.define(SeaPenRouterElement.is, SeaPenRouterElement);