chromium/chrome/browser/resources/new_tab_page/background_manager.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 {skColorToRgba} from 'chrome://resources/js/color_utils.js';
import {EventTracker} from 'chrome://resources/js/event_tracker.js';
import {PromiseResolver} from 'chrome://resources/js/promise_resolver.js';
import type {SkColor} from 'chrome://resources/mojo/skia/public/mojom/skcolor.mojom-webui.js';

import type {BackgroundImage} from './new_tab_page.mojom-webui.js';
import {strictQuery} from './utils.js';
import {WindowProxy} from './window_proxy.js';

/**
 * @fileoverview The background manager brokers access to background related
 * DOM elements. The reason for this abstraction is that the these elements are
 * not owned by any custom elements (this is done so that the aforementioned DOM
 * elements load faster at startup).
 *
 * The background manager expects an iframe with ID 'backgroundImage' to be
 * present in the DOM. It will use that element to set the background image URL.
 */

/**
 * Installs a listener for background image load times and manages a
 * |PromiseResolver| that resolves to the captured load time.
 */
class LoadTimeResolver {
  private resolver_: PromiseResolver<number> = new PromiseResolver();
  private eventTracker_: EventTracker = new EventTracker();

  constructor(url: string) {
    this.eventTracker_.add(window, 'message', ({data}: MessageEvent) => {
      if (data.frameType === 'background-image' &&
          data.messageType === 'loaded' && url === data.url) {
        this.resolve_(data.time);
      }
    });
  }

  get promise(): Promise<number> {
    return this.resolver_.promise;
  }

  reject() {
    this.resolver_.reject();
    this.eventTracker_.removeAll();
  }

  private resolve_(loadTime: number) {
    this.resolver_.resolve(loadTime);
    this.eventTracker_.removeAll();
  }
}

let instance: BackgroundManager|null = null;

export class BackgroundManager {
  static getInstance(): BackgroundManager {
    return instance || (instance = new BackgroundManager());
  }

  static setInstance(newInstance: BackgroundManager) {
    instance = newInstance;
  }

  private backgroundImage_: HTMLIFrameElement;
  private loadTimeResolver_: LoadTimeResolver|null = null;
  private url_: string;

  constructor() {
    this.backgroundImage_ =
        strictQuery(document.body, '#backgroundImage', HTMLIFrameElement);
    this.url_ = this.backgroundImage_.src;
  }

  /**
   * Sets whether the background image should be shown.
   * @param show True, if the background image should be shown.
   */
  setShowBackgroundImage(show: boolean) {
    document.body.toggleAttribute('show-background-image', show);
  }

  /** Sets the background color. */
  setBackgroundColor(color: SkColor) {
    document.body.style.backgroundColor = skColorToRgba(color);
  }

  /** Sets the background image. */
  setBackgroundImage(image: BackgroundImage) {
    const url =
        new URL('chrome-untrusted://new-tab-page/custom_background_image');
    url.searchParams.append('url', image.url.url);
    if (image.url2x) {
      url.searchParams.append('url2x', image.url2x.url);
    }
    if (image.size) {
      url.searchParams.append('size', image.size);
    }
    if (image.repeatX) {
      url.searchParams.append('repeatX', image.repeatX);
    }
    if (image.repeatY) {
      url.searchParams.append('repeatY', image.repeatY);
    }
    if (image.positionX) {
      url.searchParams.append('positionX', image.positionX);
    }
    if (image.positionY) {
      url.searchParams.append('positionY', image.positionY);
    }
    if (url.href === this.url_) {
      return;
    }
    if (this.loadTimeResolver_) {
      this.loadTimeResolver_.reject();
      this.loadTimeResolver_ = null;
    }
    // We use |contentWindow.location.replace| because reloading the iframe by
    // setting its |src| adds a history entry.
    this.backgroundImage_.contentWindow!.location.replace(url.href);
    // We track the URL separately because |contentWindow.location.replace| does
    // not update the iframe's src attribute.
    this.url_ = url.href;
  }

  /**
   * Returns promise that resolves with the background image load time.
   *
   * The background image iframe proactively sends the load time as soon as it
   * has loaded. However, this could be before we have installed the message
   * listener in LoadTimeResolver. Therefore, we request the background image
   * iframe to resend the load time in case it has already loaded. With that
   * setup we ensure that the load time is (re)sent _after_ both the NTP top
   * frame and the background image iframe have installed the required message
   * listeners.
   */
  getBackgroundImageLoadTime(): Promise<number> {
    if (!this.loadTimeResolver_) {
      this.loadTimeResolver_ = new LoadTimeResolver(this.backgroundImage_.src);
      WindowProxy.getInstance().postMessage(
          this.backgroundImage_, 'sendLoadTime',
          'chrome-untrusted://new-tab-page');
    }
    return this.loadTimeResolver_.promise;
  }
}