Repository: wagtail/wagtail
File: client/src/controllers/PreviewController.ts (TypeScript)

Code before:

    const wordCount = getWordCount(content.lang, content.innerText);
    const readingTime = getReadingTime(content.lang, wordCount);
    const metrics = { wordCount, readingTime };

    this.dispatch('content', { detail: { content, metrics } });

    renderContentMetrics(metrics);
  }

  /**
   * Extracts the rendered content from the preview iframe via an Axe plugin.
   * @param options Options object for extracting the content. Supported options:
   * - `targetElement`: CSS selector for the element to extract content from. Defaults to `main, [role="main"]`.
   * @returns An `ExtractedContent` object with `lang`, `innerText`, and `innerHTML` properties.
   */
  async extractContent(options?: ContentExtractorOptions) {
    if (!this.ready) {
      // Preview panel likely hasn't been opened, force an update to ensure
      // the preview iframe is loaded with the current data.
      await this.checkAndUpdatePreview();
      await this.reloadPromise;
    }

    return getPreviewContent(options || this.contentExtractorOptions);
  }

  /**
   * Resets the preview panel state to be ready for the next update.
   */
  finishUpdate() {
    if (this.spinnerTimeout) {
      clearTimeout(this.spinnerTimeout);
      this.spinnerTimeout = null;
    }
    if (this.hasSpinnerTarget) {
      this.spinnerTarget.hidden = true;
    }
    if (this.hasWProgressOutlet) {
      this.wProgressOutlet.loadingValue = false;
    }
    this.updatePromise = null;

    // Ensure the width is set to the default size if the preview is unavailable,
    // or the currently selected device size if the preview is available.
    this.setPreviewWidth();

    if (!this.ready) {
      this.ready = true;
      this.dispatch('ready', { cancelable: false });
    }

Review comment (suggestion):
Two promises:
- One for the `POST` request to update the preview content, resolved after the backend returns a response. May result in the reloading of an iframe, may also not (e.g. data is invalid).
- One for reloading the iframe, resolved after the new iframe has dispatched the `load` event and scroll restoration has finished.

Code after:
    // previewed page shows an error response), skip doing anything with it.
    if (!content) return;

    const wordCount = getWordCount(content.lang, content.innerText);
    const readingTime = getReadingTime(content.lang, wordCount);
    const lixScore = getLIXScore(content.lang, content.innerText);
    const readabilityScore = getReadabilityScore(lixScore);
    const metrics = { wordCount, readingTime, lixScore, readabilityScore };

    this.dispatch('content', { detail: { content, metrics } });

    renderContentMetrics(metrics);
  }

  /**
   * Extracts the rendered content from the preview iframe via an Axe plugin.
   * @param options Options object for extracting the content. Supported options:
   * - `targetElement`: CSS selector for the element to extract content from. Defaults to `main, [role="main"]`.
   * @returns An `ExtractedContent` object with `lang`, `innerText`, and `innerHTML` properties.
   */
  async extractContent(options?: ContentExtractorOptions) {
    if (!this.ready) {
      // Preview panel likely hasn't been opened, force an update to ensure
      // the preview iframe is loaded with the current data.
      await this.checkAndUpdatePreview();
      await this.reloadPromise;
    }

    return getPreviewContent(options || this.contentExtractorOptions);
  }

  /**
   * Resets the preview panel state to be ready for the next update.
   */
  finishUpdate() {
    if (this.spinnerTimeout) {
      clearTimeout(this.spinnerTimeout);
      this.spinnerTimeout = null;
    }
    if (this.hasSpinnerTarget) {
      this.spinnerTarget.hidden = true;
    }
    if (this.hasWProgressOutlet) {
      this.wProgressOutlet.loadingValue = false;
    }
    this.updatePromise = null;

    // Ensure the width is set to the default size if the preview is unavailable,
    // or the currently selected device size if the preview is available.
    this.setPreviewWidth();