chromium/chrome/browser/resources/side_panel/reading_list/reading_list_item.ts

// Copyright 2020 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/cr_elements/cr_url_list_item/cr_url_list_item.js';
import 'chrome://resources/cr_elements/cr_icon_button/cr_icon_button.js';
import 'chrome://resources/cr_elements/icons_lit.html.js';
import './icons.html.js';

import type {CrUrlListItemElement} from 'chrome://resources/cr_elements/cr_url_list_item/cr_url_list_item.js';
import {MouseHoverableMixinLit} from 'chrome://resources/cr_elements/mouse_hoverable_mixin_lit.js';
import {assertNotReached} from 'chrome://resources/js/assert.js';
import {CrLitElement} from 'chrome://resources/lit/v3_0/lit.rollup.js';

import type {ReadLaterEntry} from './reading_list.mojom-webui.js';
import type {ReadingListApiProxy} from './reading_list_api_proxy.js';
import {ReadingListApiProxyImpl} from './reading_list_api_proxy.js';
import {getCss} from './reading_list_item.css.js';
import {getHtml} from './reading_list_item.html.js';

export const MARKED_AS_READ_UI_EVENT = 'reading-list-marked-as-read';

const navigationKeys: Set<string> =
    new Set([' ', 'Enter', 'ArrowRight', 'ArrowLeft']);

export interface ReadingListItemElement {
  $: {
    crUrlListItem: CrUrlListItemElement,
    updateStatusButton: HTMLElement,
    deleteButton: HTMLElement,
  };
}

const ReadingListItemElementBase = MouseHoverableMixinLit(CrLitElement);

export class ReadingListItemElement extends ReadingListItemElementBase {
  static get is() {
    return 'reading-list-item';
  }

  static override get styles() {
    return getCss();
  }

  override render() {
    return getHtml.bind(this)();
  }

  static override get properties() {
    return {
      data: {type: Object},
      buttonRipples: {type: Boolean},
    };
  }

  data: ReadLaterEntry = {
    title: '',
    url: {url: ''},
    displayUrl: '',
    updateTime: 0n,
    read: false,
    displayTimeSinceUpdate: '',
  };

  buttonRipples: boolean = false;
  private apiProxy_: ReadingListApiProxy =
      ReadingListApiProxyImpl.getInstance();

  override firstUpdated() {
    this.addEventListener('click', this.onClick_);
    this.addEventListener('auxclick', this.onAuxClick_.bind(this));
    this.addEventListener('contextmenu', this.onContextMenu_.bind(this));
    this.addEventListener('keydown', this.onKeyDown_.bind(this));
  }

  override focus() {
    this.$.crUrlListItem.focus();
  }

  private onAuxClick_(e: MouseEvent) {
    if (e.button !== 1) {
      // Not a middle click.
      return;
    }

    this.apiProxy_.openUrl(this.data.url, true, {
      middleButton: true,
      altKey: e.altKey,
      ctrlKey: e.ctrlKey,
      metaKey: e.metaKey,
      shiftKey: e.shiftKey,
    });
  }

  private onClick_(e: MouseEvent|KeyboardEvent) {
    this.apiProxy_.openUrl(this.data.url, true, {
      middleButton: false,
      altKey: e.altKey,
      ctrlKey: e.ctrlKey,
      metaKey: e.metaKey,
      shiftKey: e.shiftKey,
    });
  }

  private onContextMenu_(e: MouseEvent) {
    this.apiProxy_.showContextMenuForUrl(this.data.url, e.clientX, e.clientY);
  }

  private onKeyDown_(e: KeyboardEvent) {
    if (e.shiftKey || !navigationKeys.has(e.key)) {
      return;
    }

    const focusableElements: HTMLElement[] = [
      this.$.crUrlListItem,
      this.$.updateStatusButton,
      this.$.deleteButton,
    ];
    const focusedIndex = focusableElements.indexOf(
        this.shadowRoot!.activeElement as HTMLElement);

    switch (e.key) {
      case ' ':
      case 'Enter':
        this.onClick_(e);
        break;
      case 'ArrowRight': {
        const index =
            focusedIndex >= focusableElements.length - 1 ? 0 : focusedIndex + 1;
        focusableElements[index]!.focus();
        break;
      }
      case 'ArrowLeft': {
        const index =
            focusedIndex <= 0 ? focusableElements.length - 1 : focusedIndex - 1;
        focusableElements[index]!.focus();
        break;
      }
      default:
        assertNotReached();
    }
    e.preventDefault();
    e.stopPropagation();
  }

  protected onUpdateStatusClick_(e: Event) {
    e.stopPropagation();
    this.apiProxy_.updateReadStatus(this.data.url, !this.data.read);
    if (!this.data.read) {
      this.dispatchEvent(new CustomEvent(
          MARKED_AS_READ_UI_EVENT, {bubbles: true, composed: true}));
    }
  }

  protected onItemDeleteClick_(e: Event) {
    e.stopPropagation();
    this.apiProxy_.removeEntry(this.data.url);
  }

  /**
   * @return The appropriate icon for the current state
   */
  protected getUpdateStatusButtonIcon_(
      markAsUnreadIcon: string, markAsReadIcon: string): string {
    return this.data.read ? markAsUnreadIcon : markAsReadIcon;
  }

  /**
   * @return The appropriate tooltip for the current state
   */
  protected getUpdateStatusButtonTooltip_(
      markAsUnreadTooltip: string, markAsReadTooltip: string): string {
    return this.data.read ? markAsUnreadTooltip : markAsReadTooltip;
  }
}

declare global {
  interface HTMLElementTagNameMap {
    'reading-list-item': ReadingListItemElement;
  }
  interface HTMLElementEventMap {
    [MARKED_AS_READ_UI_EVENT]: CustomEvent;
  }
}

customElements.define(ReadingListItemElement.is, ReadingListItemElement);