wagtail/docs/extending/editor_api.md

5.9 KiB

Accessing the editor programmatically

Wagtail's editor is built with various JavaScript components that can be interacted with programmatically. This document provides an overview of how to access and extend the editor's functionality.

The editor's <form> element

The editor's main <form> element can be queried using the data-edit-form attribute. This is useful for attaching event listeners or manipulating the form programmatically, as well as getting the form's FormData representation.

const editForm = document.querySelector('form[data-edit-form]');
const data = new FormData(editForm);

The preview panel

The preview panel is powered by the PreviewController and its instance can be accessed using the wagtail.app.queryController function. The PreviewController provides methods to control the preview, such as extracting the previewed content and running content checks. Refer to the PreviewController documentation for more details.

const previewController = window.wagtail.app.queryController('w-preview');
const content = await previewController?.extractContent();
await previewController?.runContentChecks();

Example: generating meta description

Extracting the previewed content using the PreviewController can be useful for different use cases. One example is generating a meta description for the page using a Large Language Model (LLM). The following example demonstrates a custom Stimulus controller that uses an LLM from the browser's Summarizer API to generate the page's meta description.

/* js/summarize.js */

class SummarizeController extends window.StimulusModule.Controller {
  static targets = ['suggest'];

  static values = {
    input: { default: '', type: String },
  };

  /** Only load the controller if the browser supports the Summarizer API. */
  static get shouldLoad() {
    return 'Summarizer' in window;
  }

  /** The previewed content's language. */
  contentLanguage = document.documentElement.lang || 'en';
  /** A cached Summarizer instance Promise to avoid recreating it unnecessarily. */
  #summarizer = null;

  /** Promise of a browser Summarizer instance. */
  get summarizer() {
    if (this.#summarizer) return this.#summarizer; // Return from cache
    this.#summarizer = Summarizer.create({
      // Change the Summarizer's configuration as needed
      sharedContext: `A summary of a webpage's content, suitable for use as a meta description.`,
      type: 'teaser',
      length: 'short',
      format: 'plain-text',
      expectedInputLanguages: [this.contentLanguage],
      outputLanguage: document.documentElement.lang,
    });
    return this.#summarizer;
  }

  connect() {
    this.input = this.element.querySelector(this.inputValue);
    this.renderFurniture();
  }

  renderFurniture() {
    const prefix = this.element.closest('[id]').id;
    const buttonId = `${prefix}-generate`;
    const button = /* html */ `
      <button
        id="${buttonId}"
        type="button"
        data-summarize-target="suggest"
        data-action="summarize#generate"
        class="button"
      >
        Generate suggestions
      </button>
    `;
    this.element.insertAdjacentHTML('beforeend', button);

    this.outputArea = document.createElement('div');
    this.element.append(this.outputArea);
  }

  renderSuggestion(suggestion) {
    const template = document.createElement('template');
    template.innerHTML = /* html */ `
      <div>
        <output for="${this.suggestTarget.id}">${suggestion}</output>
        <button class="button button-small" type="button" data-action="summarize#useSuggestion">Use</button>
      </div>
    `;
    this.outputArea.append(template.content.firstElementChild);
  }

  useSuggestion(event) {
    this.input.value = event.target.previousElementSibling.textContent;
  }

  async summarize(text) {
    const summarizer = await this.summarizer;
    return summarizer.summarize(text);
  }

  async getPageContent() {
    const previewController = window.wagtail.app.queryController('w-preview');
    const { innerText, lang } = await previewController.extractContent();
    this.contentLanguage = lang;
    return innerText;
  }

  async generate() {
    this.outputArea.innerHTML = '';
    this.suggestTarget.textContent = 'Generating…';
    this.suggestTarget.disabled = true;

    const text = await this.getPageContent();
    await Promise.allSettled(
      [...Array(3).keys()].map(() =>
        this.summarize(text)
          .then((output) => this.renderSuggestion(output))
          .catch((error) => {
            console.error('Error generating suggestion:', error);
          }),
      ),
    );

    this.suggestTarget.disabled = false;
    this.suggestTarget.textContent = 'Generate suggestions';
  }
}

window.wagtail.app.register('summarize', SummarizeController);

The JavaScript file can be loaded to the editor using the insert_editor_js hook and attached to the Page's FieldPanel for the search_description field:

# myapp/wagtail_hooks.py
from django.templatetags.static import static
from django.utils.html import format_html_join
from wagtail import hooks
from wagtail.admin.panels import FieldPanel
from wagtail.models import Page


@hooks.register("insert_editor_js")
def editor_js():
    js_files = ["js/summarize.js"]
    return format_html_join(
        "\n",
        '<script src="{}"></script>',
        ((static(filename),) for filename in js_files),
    )

# Replace the default `FieldPanel` for `search_description`
# with a custom one that uses the `summarize` controller.
Page.promote_panels[0].args[0][-1] = FieldPanel(
    "search_description",
    attrs={
        "data-controller": "summarize",
        "data-summarize-input-value": "[name='search_description']",
    },
)