chromium/chrome/browser/resources/bookmarks/api_listener.ts

// Copyright 2017 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 {addWebUiListener, removeWebUiListener} from 'chrome://resources/js/cr.js';
import type {Action} from 'chrome://resources/js/store.js';

import {createBookmark, editBookmark, moveBookmark, refreshNodes, removeBookmark, reorderChildren, setCanEditBookmarks, setIncognitoAvailability} from './actions.js';
import {BrowserProxyImpl} from './browser_proxy.js';
import type {IncognitoAvailability} from './constants.js';
import {Debouncer} from './debouncer.js';
import {Store} from './store.js';
import {normalizeNodes} from './util.js';

/**
 * @fileoverview Listener functions which translate events from the
 * chrome.bookmarks API into actions to modify the local page state.
 */

let trackUpdates: boolean = false;
let updatedItems: string[] = [];

let debouncer: Debouncer|null = null;

/**
 * Batches UI updates so that no changes will be made to UI until the next
 * task after the last call to this method. This is useful for listeners which
 * can be called in a tight loop by UI actions.
 */
function batchUIUpdates() {
  if (debouncer === null) {
    debouncer = new Debouncer(() => Store.getInstance().endBatchUpdate());
  }

  if (debouncer.done()) {
    Store.getInstance().beginBatchUpdate();
    debouncer.reset();
  }

  debouncer.restartTimeout();
}

/**
 * Tracks any items that are created or moved.
 */
export function trackUpdatedItems() {
  trackUpdates = true;
}

function highlightUpdatedItemsImpl() {
  if (!trackUpdates) {
    return;
  }

  document.dispatchEvent(new CustomEvent('highlight-items', {
    detail: updatedItems,
  }));
  updatedItems = [];
  trackUpdates = false;
}

/**
 * Highlights any items that have been updated since |trackUpdatedItems| was
 * called. Should be called after a user action causes new items to appear in
 * the main list.
 */
export function highlightUpdatedItems() {
  // Ensure that the items are highlighted after the current batch update (if
  // there is one) is completed.
  assert(debouncer);
  debouncer.promise.then(highlightUpdatedItemsImpl);
}

function dispatch(action: Action) {
  Store.getInstance().dispatch(action);
}

function onBookmarkChanged(
    id: string, changeInfo: chrome.bookmarks.ChangeInfo) {
  dispatch(editBookmark(id, changeInfo));
}

function onBookmarkCreated(
    id: string, treeNode: chrome.bookmarks.BookmarkTreeNode) {
  batchUIUpdates();
  if (trackUpdates) {
    updatedItems.push(id);
  }
  dispatch(createBookmark(id, treeNode));
}

function onBookmarkRemoved(
    id: string, removeInfo: chrome.bookmarks.RemoveInfo) {
  batchUIUpdates();
  const nodes = Store.getInstance().data.nodes;
  dispatch(removeBookmark(id, removeInfo.parentId, removeInfo.index, nodes));
}

function onBookmarkMoved(id: string, moveInfo: chrome.bookmarks.MoveInfo) {
  batchUIUpdates();
  if (trackUpdates) {
    updatedItems.push(id);
  }
  dispatch(moveBookmark(
      id, moveInfo.parentId, moveInfo.index, moveInfo.oldParentId,
      moveInfo.oldIndex));
}

function onChildrenReordered(
    id: string, reorderInfo: chrome.bookmarks.ReorderInfo) {
  dispatch(reorderChildren(id, reorderInfo.childIds));
}

/**
 * Pauses the Created handler during an import. The imported nodes will all be
 * loaded at once when the import is finished.
 */
function onImportBegan() {
  chrome.bookmarks.onCreated.removeListener(onBookmarkCreated);
  document.dispatchEvent(new CustomEvent('import-began'));
}

function onImportEnded() {
  chrome.bookmarks.getTree().then((results) => {
    dispatch(refreshNodes(normalizeNodes(results[0]!)));
  });
  chrome.bookmarks.onCreated.addListener(onBookmarkCreated);
  document.dispatchEvent(new CustomEvent('import-ended'));
}

function onIncognitoAvailabilityChanged(availability: IncognitoAvailability) {
  dispatch(setIncognitoAvailability(availability));
}

function onCanEditBookmarksChanged(canEdit: boolean) {
  dispatch(setCanEditBookmarks(canEdit));
}

let incognitoAvailabilityListener: {eventName: string, uid: number}|null = null;

let canEditBookmarksListener: {eventName: string, uid: number}|null = null;

export function init() {
  chrome.bookmarks.onChanged.addListener(onBookmarkChanged);
  chrome.bookmarks.onChildrenReordered.addListener(onChildrenReordered);
  chrome.bookmarks.onCreated.addListener(onBookmarkCreated);
  chrome.bookmarks.onMoved.addListener(onBookmarkMoved);
  chrome.bookmarks.onRemoved.addListener(onBookmarkRemoved);
  chrome.bookmarks.onImportBegan.addListener(onImportBegan);
  chrome.bookmarks.onImportEnded.addListener(onImportEnded);

  const browserProxy = BrowserProxyImpl.getInstance();
  browserProxy.getIncognitoAvailability().then(onIncognitoAvailabilityChanged);
  incognitoAvailabilityListener = addWebUiListener(
      'incognito-availability-changed', onIncognitoAvailabilityChanged);

  browserProxy.getCanEditBookmarks().then(onCanEditBookmarksChanged);
  canEditBookmarksListener =
      addWebUiListener('can-edit-bookmarks-changed', onCanEditBookmarksChanged);
}

export function destroy() {
  chrome.bookmarks.onChanged.removeListener(onBookmarkChanged);
  chrome.bookmarks.onChildrenReordered.removeListener(onChildrenReordered);
  chrome.bookmarks.onCreated.removeListener(onBookmarkCreated);
  chrome.bookmarks.onMoved.removeListener(onBookmarkMoved);
  chrome.bookmarks.onRemoved.removeListener(onBookmarkRemoved);
  chrome.bookmarks.onImportBegan.removeListener(onImportBegan);
  chrome.bookmarks.onImportEnded.removeListener(onImportEnded);
  if (incognitoAvailabilityListener) {
    removeWebUiListener(/** @type {{eventName: string, uid: number}} */ (
        incognitoAvailabilityListener));
  }
  if (canEditBookmarksListener) {
    removeWebUiListener(/** @type {{eventName: string, uid: number}} */ (
        canEditBookmarksListener));
  }
}

export function setDebouncerForTesting() {
  debouncer = new Debouncer(() => {});
}